tksbrokerapi.TKSBrokerAPI

TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios, as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: from the console, it has a rich keys and commands, or you can use it as Python module with python import.

TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.

   1# -*- coding: utf-8 -*-
   2# Author: Timur Gilmullin
   3
   4"""
   5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios,
   6as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
   7from the console, it has a rich keys and commands, or you can use it as Python module with `python import`.
   8
   9TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive
  10the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
  11
  12- **Open account for trading:** http://tinkoff.ru/sl/AaX1Et1omnH
  13- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
  14- **See CLI examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
  15- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
  16- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/
  17- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/
  18"""
  19
  20# Copyright (c) 2022 Gilmillin Timur Mansurovich
  21#
  22# Licensed under the Apache License, Version 2.0 (the "License");
  23# you may not use this file except in compliance with the License.
  24# You may obtain a copy of the License at
  25#
  26#     http://www.apache.org/licenses/LICENSE-2.0
  27#
  28# Unless required by applicable law or agreed to in writing, software
  29# distributed under the License is distributed on an "AS IS" BASIS,
  30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  31# See the License for the specific language governing permissions and
  32# limitations under the License.
  33
  34
  35import sys
  36import os
  37from argparse import ArgumentParser
  38from importlib.metadata import version
  39
  40from dateutil.tz import tzlocal
  41from time import sleep
  42
  43import re
  44import json
  45import requests
  46import traceback as tb
  47from typing import Union
  48
  49from multiprocessing import cpu_count
  50from multiprocessing.pool import ThreadPool
  51import pandas as pd
  52
  53from TKSEnums import *  # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/
  54from TradeRoutines import *  # This library contains some methods used by trade scenarios implemented with TKSBrokerAPI module
  55
  56from pricegenerator.PriceGenerator import PriceGenerator, uLogger  # This module has a lot of instruments to work with candles data. See docs here: https://github.com/Tim55667757/PriceGenerator
  57from pricegenerator.UniLogger import DisableLogger as PGDisLog  # Method for disable log from PriceGenerator
  58
  59import UniLogger as uLog  # Logger for TKSBrokerAPI
  60
  61
  62# --- Common technical parameters:
  63
  64PGDisLog(uLogger.handlers[0])  # Disable 3-rd party logging from PriceGenerator
  65uLogger = uLog.UniLogger  # init logger for TKSBrokerAPI
  66uLogger.level = 10  # debug level by default for TKSBrokerAPI module
  67uLogger.handlers[0].level = 20  # info level by default for STDOUT of TKSBrokerAPI module
  68
  69__version__ = "1.5"  # The "major.minor" version setup here, but build number define at the build-server only
  70
  71CPU_COUNT = cpu_count()  # host's real CPU count
  72CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1  # how many CPUs will be used for parallel calculations
  73
  74
  75class TinkoffBrokerServer:
  76    """
  77    This class implements methods to work with Tinkoff broker server.
  78
  79    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
  80
  81    About `token`: https://tinkoff.github.io/investAPI/token/
  82    """
  83    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
  84        """
  85        Main class init.
  86
  87        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
  88        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
  89                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
  90        :param useCache: use default cache file with raw data to use instead of `iList`.
  91                         True by default. Cache is auto-update if new day has come.
  92                         If you don't want to use cache and always updates raw data then set `useCache=False`.
  93        :param defaultCache: path to default cache file. `dump.json` by default.
  94        """
  95        if token is None or not token:
  96            try:
  97                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
  98                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
  99
 100            except KeyError:
 101                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 102                raise Exception("Token required")
 103
 104        else:
 105            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 106            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 107
 108        if accountId is None or not accountId:
 109            try:
 110                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 111                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 112
 113            except KeyError:
 114                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 115
 116        else:
 117            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 118            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 119
 120        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 121        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 122
 123        Latest version: https://pypi.org/project/tksbrokerapi/
 124        """
 125
 126        self.aliases = TKS_TICKER_ALIASES
 127        """Some aliases instead official tickers.
 128
 129        See also: `TKSEnums.TKS_TICKER_ALIASES`
 130        """
 131
 132        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 133
 134        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 135
 136        self.ticker = ""
 137        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 138
 139        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 140        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 141
 142        See also: `SearchByTicker()`, `SearchInstruments()`.
 143        """
 144
 145        self.figi = ""
 146        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 147
 148        See also: `SearchByFIGI()`, `SearchInstruments()`.
 149        """
 150
 151        self.depth = 1
 152        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 153
 154        See also: `GetCurrentPrices()`.
 155        """
 156
 157        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 158        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 159
 160        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 161        """
 162
 163        uLogger.debug("Broker API server: {}".format(self.server))
 164
 165        self.timeout = 15
 166        """Server operations timeout in seconds. Default: `15`.
 167
 168        See also: `SendAPIRequest()`.
 169        """
 170
 171        self.headers = {
 172            "Content-Type": "application/json",
 173            "accept": "application/json",
 174            "Authorization": "Bearer {}".format(self.token),
 175            "x-app-name": "Tim55667757.TKSBrokerAPI",
 176        }
 177        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 178
 179        See also: `SendAPIRequest()`.
 180        """
 181
 182        self.body = None
 183        """Request body which send to broker server. Default: `None`.
 184
 185        See also: `SendAPIRequest()`.
 186        """
 187
 188        self.moreDebug = False
 189        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
 190
 191        self.historyFile = None
 192        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 193
 194        See also: `History()`.
 195        """
 196
 197        self.htmlHistoryFile = "index.html"
 198        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 199
 200        See also: `ShowHistoryChart()`.
 201        """
 202
 203        self.instrumentsFile = "instruments.md"
 204        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 205
 206        See also: `ShowInstrumentsInfo()`.
 207        """
 208
 209        self.searchResultsFile = "search-results.md"
 210        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 211
 212        See also: `SearchInstruments()`.
 213        """
 214
 215        self.pricesFile = "prices.md"
 216        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 217
 218        See also: `GetListOfPrices()`.
 219        """
 220
 221        self.infoFile = "info.md"
 222        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 223
 224        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 225        """
 226
 227        self.bondsXLSXFile = "ext-bonds.xlsx"
 228        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 229        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 230
 231        See also: `ExtendBondsData()`.
 232        """
 233
 234        self.calendarFile = "calendar.md"
 235        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 236        
 237        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 238
 239        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 240        """
 241
 242        self.overviewFile = "overview.md"
 243        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 244
 245        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 246        """
 247
 248        self.overviewDigestFile = "overview-digest.md"
 249        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 250
 251        See also: `Overview()` with parameter `details="digest"`.
 252        """
 253
 254        self.overviewPositionsFile = "overview-positions.md"
 255        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 256
 257        See also: `Overview()` with parameter `details="positions"`.
 258        """
 259
 260        self.overviewOrdersFile = "overview-orders.md"
 261        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 262
 263        See also: `Overview()` with parameter `details="orders"`.
 264        """
 265
 266        self.overviewAnalyticsFile = "overview-analytics.md"
 267        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 268
 269        See also: `Overview()` with parameter `details="analytics"`.
 270        """
 271
 272        self.overviewBondsCalendarFile = "overview-calendar.md"
 273        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
 274
 275        See also: `Overview()` with parameter `details="calendar"`.
 276        """
 277
 278        self.reportFile = "deals.md"
 279        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 280
 281        See also: `Deals()`.
 282        """
 283
 284        self.withdrawalLimitsFile = "limits.md"
 285        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 286
 287        See also: `OverviewLimits()` and `RequestLimits()`.
 288        """
 289
 290        self.userInfoFile = "user-info.md"
 291        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 292
 293        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 294        """
 295
 296        self.userAccountsFile = "accounts.md"
 297        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 298
 299        See also: `OverviewAccounts()`, `RequestAccounts()`.
 300        """
 301
 302        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 303        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 304
 305        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 306
 307        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 308        """
 309
 310        self.iList = None  # init iList for raw instruments data
 311        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 312        
 313        See also: `Listing()`, `DumpInstruments()`.
 314        """
 315
 316        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 317        if useCache:
 318            if os.path.exists(self.iListDumpFile):
 319                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 320                curTime = datetime.now(tzutc())
 321
 322                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 323                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 324
 325                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 326
 327                else:
 328                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 329
 330                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
 331                        os.path.abspath(self.iListDumpFile),
 332                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
 333                    ))
 334
 335            else:
 336                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 337                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 338
 339        else:
 340            self.iList = self.Listing()  # request new raw instruments data from broker server
 341            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 342
 343        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 344        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 345
 346        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 347        """
 348
 349    def _ParseJSON(self, rawData="{}") -> dict:
 350        """
 351        Parse JSON from response string.
 352
 353        :param rawData: this is a string with JSON-formatted text.
 354        :return: JSON (dictionary), parsed from server response string.
 355        """
 356        responseJSON = json.loads(rawData) if rawData else {}
 357
 358        if self.moreDebug:
 359            uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
 360
 361        return responseJSON
 362
 363    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
 364        """
 365        Send GET or POST request to broker server and receive JSON object.
 366
 367        self.header: must be defining with dictionary of headers.
 368        self.body: if define then used as request body. None by default.
 369        self.timeout: global request timeout, 15 seconds by default.
 370        :param url: url with REST request.
 371        :param reqType: send "GET" or "POST" request. "GET" by default.
 372        :param retry: how many times retry after first request if an 5xx server errors occurred.
 373        :param pause: sleep time in seconds between retries.
 374        :return: response JSON (dictionary) from broker.
 375        """
 376        if reqType not in ("GET", "POST"):
 377            uLogger.error("You can define request type: 'GET' or 'POST'!")
 378            raise Exception("Incorrect value")
 379
 380        if self.moreDebug:
 381            uLogger.debug("Request parameters:")
 382            uLogger.debug("    - REST API URL: {}".format(url))
 383            uLogger.debug("    - request type: {}".format(reqType))
 384            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
 385            uLogger.debug("    - body:\n{}".format(self.body))
 386
 387        # fast hack to avoid all operations with some tickers/FIGI
 388        responseJSON = {}
 389        oK = True
 390        for item in self.exclude:
 391            if item in url:
 392                if self.moreDebug:
 393                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 394
 395                oK = False
 396                break
 397
 398        if oK:
 399            counter = 0
 400            response = None
 401            errMsg = ""
 402
 403            while not response and counter <= retry:
 404                if reqType == "GET":
 405                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 406
 407                if reqType == "POST":
 408                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 409
 410                if self.moreDebug:
 411                    uLogger.debug("Response:")
 412                    uLogger.debug("    - status code: {}".format(response.status_code))
 413                    uLogger.debug("    - reason: {}".format(response.reason))
 414                    uLogger.debug("    - body length: {}".format(len(response.text)))
 415                    uLogger.debug("    - headers:\n{}".format(response.headers))
 416
 417                # Server returns some headers:
 418                # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
 419                # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
 420                # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
 421                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 422                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 423                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
 424                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 425                    sleep(rateLimitWait)
 426
 427                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 428                if 400 <= response.status_code < 500:
 429                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 430                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 431                    counter = retry + 1
 432
 433                if 500 <= response.status_code < 600:
 434                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 435                    uLogger.debug("    - not oK, {}".format(errMsg))
 436                    counter += 1
 437
 438                    if counter <= retry:
 439                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 440                        sleep(pause)
 441
 442            responseJSON = self._ParseJSON(rawData=response.text)
 443
 444            if errMsg:
 445                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 446                uLogger.error("    - not oK, {}".format(errMsg))
 447
 448        return responseJSON
 449
 450    def _IUpdater(self, iType: str) -> tuple:
 451        """
 452        Request instrument by type from server. See available API methods for instruments:
 453        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 454        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 455        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 456        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 457        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 458
 459        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 460        :return: tuple with iType name and list of available instruments of current type for defined user token.
 461        """
 462        result = []
 463
 464        if iType in TKS_INSTRUMENTS:
 465            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 466
 467            # all instruments have the same body in API v2 requests:
 468            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 469            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 470            result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
 471
 472        return iType, result
 473
 474    def _IWrapper(self, kwargs):
 475        """
 476        Wrapper runs instrument's update method `_IUpdater()`.
 477        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 478        """
 479        return self._IUpdater(**kwargs)
 480
 481    def Listing(self) -> dict:
 482        """
 483        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 484
 485        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 486        """
 487        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 488        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 489
 490        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 491        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 492        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 493
 494        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 495        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 496        poolUpdater.close()
 497
 498        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 499        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 500        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 501
 502        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 503        for iType in iList.keys():
 504            for ticker in iList[iType]:
 505                iList[iType][ticker]["type"] = iType
 506
 507                if "minPriceIncrement" in iList[iType][ticker].keys():
 508                    iList[iType][ticker]["step"] = NanoToFloat(
 509                        iList[iType][ticker]["minPriceIncrement"]["units"],
 510                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 511                    )
 512
 513                else:
 514                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 515
 516        return iList
 517
 518    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 519        """
 520        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 521
 522        See also: `DumpInstruments()`, `Listing()`.
 523
 524        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 525                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 526        """
 527        if self.iListDumpFile is None or not self.iListDumpFile:
 528            uLogger.error("Output name of dump file must be defined!")
 529            raise Exception("Filename required")
 530
 531        if not self.iList or forceUpdate:
 532            self.iList = self.Listing()
 533
 534        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 535
 536        # Save as XLSX with separated sheets for every type of instruments:
 537        with pd.ExcelWriter(
 538                path=xlsxDumpFile,
 539                date_format=TKS_DATE_FORMAT,
 540                datetime_format=TKS_DATE_TIME_FORMAT,
 541                mode="w",
 542        ) as writer:
 543            for iType in TKS_INSTRUMENTS:
 544                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 545                df = df[sorted(df)]  # sorted by column names
 546                df = df.applymap(
 547                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 548                    na_action="ignore",
 549                )  # converting numbers from nano-type to float in every cell
 550                df.to_excel(
 551                    writer,
 552                    sheet_name=iType,
 553                    encoding="UTF-8",
 554                    freeze_panes=(1, 1),
 555                )  # saving as XLSX-file with freeze first row and column as headers
 556
 557        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 558
 559    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 560        """
 561        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 562        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 563
 564        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 565
 566        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 567                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 568        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 569        """
 570        if self.iListDumpFile is None or not self.iListDumpFile:
 571            uLogger.error("Output name of dump file must be defined!")
 572            raise Exception("Filename required")
 573
 574        if not self.iList or forceUpdate:
 575            self.iList = self.Listing()
 576
 577        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 578        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 579            fH.write(jsonDump)
 580
 581        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 582
 583        return jsonDump
 584
 585    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
 586        """
 587        Show information about one instrument defined by json data and prints it in Markdown format.
 588
 589        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 590
 591        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]`
 592        :param show: if `True` then also printing information about instrument and its current price.
 593        :return: multilines text in Markdown format with information about one instrument.
 594        """
 595        splitLine = "|                                                             |                                                        |\n"
 596        infoText = ""
 597
 598        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 599            info = [
 600                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
 601                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
 602                "| Parameters                                                  | Values                                                 |\n",
 603                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 604                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 605                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 606            ]
 607
 608            if "sector" in iJSON.keys() and iJSON["sector"]:
 609                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 610
 611            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
 612                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
 613
 614            info.extend([
 615                splitLine,
 616                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 617                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
 618            ])
 619
 620            if "isin" in iJSON.keys() and iJSON["isin"]:
 621                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 622
 623            if "classCode" in iJSON.keys():
 624                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 625
 626            info.extend([
 627                splitLine,
 628                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 629                splitLine,
 630                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 631                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 632                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 633            ])
 634
 635            if iJSON["figi"]:
 636                self.figi = iJSON["figi"]
 637                iJSON = iJSON | self.RequestTradingStatus()
 638
 639                info.extend([
 640                    splitLine,
 641                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 642                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 643                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 644                ])
 645
 646            info.append(splitLine)
 647
 648            if "type" in iJSON.keys() and iJSON["type"]:
 649                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 650
 651                if "shareType" in iJSON.keys() and iJSON["shareType"]:
 652                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
 653
 654            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 655                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 656
 657            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 658                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 659
 660            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 661                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 662
 663            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 664                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 665
 666            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 667                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 668
 669            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 670                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 671
 672            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 673                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 674
 675            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 676                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 677
 678            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 679                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 680
 681            if "currency" in iJSON.keys():
 682                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 683
 684            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 685                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 686
 687            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 688                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 689
 690            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 691                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 692
 693            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 694                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 695
 696            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 697                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 698
 699            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 700                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 701
 702            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 703                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 704
 705            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 706                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 707
 708            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 709                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 710
 711            iExt = None
 712            if iJSON["type"] == "Bonds":
 713                info.extend([
 714                    splitLine,
 715                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 716                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 717                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 718                        iJSON["nominal"]["currency"],
 719                    )),
 720                ])
 721
 722                if "floatingCouponFlag" in iJSON.keys():
 723                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 724
 725                if "amortizationFlag" in iJSON.keys():
 726                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 727
 728                info.append(splitLine)
 729
 730                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 731                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 732
 733                if iJSON["figi"]:
 734                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 735
 736                    info.extend([
 737                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 738                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 739                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 740                    ])
 741
 742                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 743                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 744                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 745                        iJSON["aciValue"]["currency"]
 746                    )))
 747
 748            if "currentPrice" in iJSON.keys():
 749                info.append(splitLine)
 750
 751                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 752                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 753
 754                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 755                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 756                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 757                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 758                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 759
 760                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 761                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 762
 763                info.extend([
 764                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 765                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 766                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 767                    )),
 768                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 769                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 770                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 771                    )),
 772                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 773                        "{:.2f}%{}".format(
 774                            iJSON["currentPrice"]["changes"],
 775                            " ({}{:.2f} {})".format(
 776                                "+" if bondChangesDelta > 0 else "",
 777                                bondChangesDelta,
 778                                aciCurrency
 779                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 780                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 781                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 782                                currency
 783                            ),
 784                        )
 785                    ),
 786                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 787                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 788                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 789                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 790                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 791                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 792                    )),
 793                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 794                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 795                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 796                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 797                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 798                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 799                    )),
 800                ])
 801
 802            if "lot" in iJSON.keys():
 803                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 804
 805            if "step" in iJSON.keys() and iJSON["step"] != 0:
 806                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
 807
 808            # Add bond payment calendar:
 809            if iJSON["type"] == "Bonds":
 810                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 811                info.extend(["\n", strCalendar])
 812
 813            infoText += "".join(info)
 814
 815            if show:
 816                uLogger.info("{}".format(infoText))
 817
 818            else:
 819                uLogger.debug("{}".format(infoText))
 820
 821            if self.infoFile is not None:
 822                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 823                    fH.write(infoText)
 824
 825                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 826
 827        return infoText
 828
 829    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 830        """
 831        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 832
 833        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 834        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 835        :return: JSON formatted data with information about instrument.
 836        """
 837        tickerJSON = {}
 838        if self.moreDebug:
 839            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker))
 840
 841        if not self.ticker:
 842            uLogger.warning("self.ticker variable is not be empty!")
 843
 844        else:
 845            if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 846                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker))
 847                raise Exception("Instrument not allowed")
 848
 849            if not self.iList:
 850                self.iList = self.Listing()
 851
 852            if self.ticker in self.iList["Shares"].keys():
 853                tickerJSON = self.iList["Shares"][self.ticker]
 854                if self.moreDebug:
 855                    uLogger.debug("Ticker [{}] found in shares list".format(self.ticker))
 856
 857            elif self.ticker in self.iList["Currencies"].keys():
 858                tickerJSON = self.iList["Currencies"][self.ticker]
 859                if self.moreDebug:
 860                    uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker))
 861
 862            elif self.ticker in self.iList["Bonds"].keys():
 863                tickerJSON = self.iList["Bonds"][self.ticker]
 864                if self.moreDebug:
 865                    uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker))
 866
 867            elif self.ticker in self.iList["Etfs"].keys():
 868                tickerJSON = self.iList["Etfs"][self.ticker]
 869                if self.moreDebug:
 870                    uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker))
 871
 872            elif self.ticker in self.iList["Futures"].keys():
 873                tickerJSON = self.iList["Futures"][self.ticker]
 874                if self.moreDebug:
 875                    uLogger.debug("Ticker [{}] found in futures list".format(self.ticker))
 876
 877        if tickerJSON:
 878            self.figi = tickerJSON["figi"]
 879
 880            if requestPrice:
 881                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 882
 883                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 884                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 885
 886                else:
 887                    tickerJSON["currentPrice"]["changes"] = 0
 888
 889            if show:
 890                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 891
 892        else:
 893            if show:
 894                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker))
 895
 896        return tickerJSON
 897
 898    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 899        """
 900        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
 901
 902        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
 903        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 904        :return: JSON formatted data with information about instrument.
 905        """
 906        figiJSON = {}
 907        if self.moreDebug:
 908            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi))
 909
 910        if not self.figi:
 911            uLogger.warning("self.figi variable is not be empty!")
 912
 913        else:
 914            if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
 915                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi))
 916                raise Exception("Instrument not allowed")
 917
 918            if not self.iList:
 919                self.iList = self.Listing()
 920
 921            for item in self.iList["Shares"].keys():
 922                if self.figi == self.iList["Shares"][item]["figi"]:
 923                    figiJSON = self.iList["Shares"][item]
 924
 925                    if self.moreDebug:
 926                        uLogger.debug("FIGI [{}] found in shares list".format(self.figi))
 927
 928                    break
 929
 930            if not figiJSON:
 931                for item in self.iList["Currencies"].keys():
 932                    if self.figi == self.iList["Currencies"][item]["figi"]:
 933                        figiJSON = self.iList["Currencies"][item]
 934
 935                        if self.moreDebug:
 936                            uLogger.debug("FIGI [{}] found in currencies list".format(self.figi))
 937
 938                        break
 939
 940            if not figiJSON:
 941                for item in self.iList["Bonds"].keys():
 942                    if self.figi == self.iList["Bonds"][item]["figi"]:
 943                        figiJSON = self.iList["Bonds"][item]
 944
 945                        if self.moreDebug:
 946                            uLogger.debug("FIGI [{}] found in bonds list".format(self.figi))
 947
 948                        break
 949
 950            if not figiJSON:
 951                for item in self.iList["Etfs"].keys():
 952                    if self.figi == self.iList["Etfs"][item]["figi"]:
 953                        figiJSON = self.iList["Etfs"][item]
 954
 955                        if self.moreDebug:
 956                            uLogger.debug("FIGI [{}] found in etfs list".format(self.figi))
 957
 958                        break
 959
 960            if not figiJSON:
 961                for item in self.iList["Futures"].keys():
 962                    if self.figi == self.iList["Futures"][item]["figi"]:
 963                        figiJSON = self.iList["Futures"][item]
 964
 965                        if self.moreDebug:
 966                            uLogger.debug("FIGI [{}] found in futures list".format(self.figi))
 967
 968                        break
 969
 970        if figiJSON:
 971            self.figi = figiJSON["figi"]
 972            self.ticker = figiJSON["ticker"]
 973
 974            if requestPrice:
 975                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 976
 977                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
 978                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
 979
 980                else:
 981                    figiJSON["currentPrice"]["changes"] = 0
 982
 983            if show:
 984                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
 985
 986        else:
 987            if show:
 988                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi))
 989
 990        return figiJSON
 991
 992    def GetCurrentPrices(self, show: bool = True) -> dict:
 993        """
 994        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
 995        `{"buy": [{"price": 1243.8, "quantity": 193},
 996                  {"price": 1244.0, "quantity": 168},
 997                  {"price": 1244.8, "quantity": 5},
 998                  {"price": 1245.0, "quantity": 61},
 999                  {"price": 1245.4, "quantity": 60}],
1000          "sell": [{"price": 1243.6, "quantity": 8},
1001                   {"price": 1242.6, "quantity": 10},
1002                   {"price": 1242.4, "quantity": 18},
1003                   {"price": 1242.2, "quantity": 50},
1004                   {"price": 1242.0, "quantity": 113}],
1005          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1006        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1007        - sell: list of dicts with Buyers prices,
1008            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1009            - quantity: volume value by current price in lots,
1010        - limitUp: current trade session limit price, maximum,
1011        - limitDown: current trade session limit price, minimum,
1012        - lastPrice: last deal price of the instrument,
1013        - closePrice: previous trade session close price of the instrument.
1014
1015        See also: `SearchByTicker()` and `SearchByFIGI()`.
1016        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1017        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1018
1019        :param show: if `True` then print DOM to log and console.
1020        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1021                 If an error occurred then returns an empty record:
1022                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1023        """
1024        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1025
1026        if self.depth < 1:
1027            uLogger.error("Depth of Market (DOM) must be >=1!")
1028            raise Exception("Incorrect value")
1029
1030        if not (self.ticker or self.figi):
1031            uLogger.error("self.ticker or self.figi variables must be defined!")
1032            raise Exception("Ticker or FIGI required")
1033
1034        if self.ticker and not self.figi:
1035            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1036            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1037
1038        if not self.ticker and self.figi:
1039            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1040            self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1041
1042        if not self.figi:
1043            uLogger.error("FIGI is not defined!")
1044            raise Exception("Ticker or FIGI required")
1045
1046        else:
1047            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi))
1048
1049            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1050            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1051            self.body = str({"figi": self.figi, "depth": self.depth})
1052            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1053
1054            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1055                # list of dicts with sellers orders:
1056                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1057
1058                # list of dicts with buyers orders:
1059                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1060
1061                # max price of instrument at this time:
1062                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1063
1064                # min price of instrument at this time:
1065                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1066
1067                # last price of deal with instrument:
1068                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1069
1070                # last close price of instrument:
1071                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1072
1073            else:
1074                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1075                uLogger.debug("Server response: {}".format(pricesResponse))
1076
1077            if show:
1078                if prices["buy"] or prices["sell"]:
1079                    info = [
1080                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1081                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1082                            self.ticker,
1083                            self.figi,
1084                            self.depth,
1085                        ),
1086                        "-" * 60, "\n",
1087                        "             Orders of Buyers | Orders of Sellers\n",
1088                        "-" * 60, "\n",
1089                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1090                        "-" * 60, "\n",
1091                    ]
1092
1093                    if not prices["buy"]:
1094                        info.append("                              | No orders!\n")
1095                        sumBuy = 0
1096
1097                    else:
1098                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1099                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1100                        for item in maxMinSorted:
1101                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1102
1103                    if not prices["sell"]:
1104                        info.append("No orders!                    |\n")
1105                        sumSell = 0
1106
1107                    else:
1108                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1109                        for item in prices["sell"]:
1110                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1111
1112                    info.extend([
1113                        "-" * 60, "\n",
1114                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1115                        "-" * 60, "\n",
1116                    ])
1117
1118                    infoText = "".join(info)
1119
1120                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1121
1122                else:
1123                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1124
1125        return prices
1126
1127    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1128        """
1129        This method get and show information about all available broker instruments for current user account.
1130        If `instrumentsFile` string is not empty then also save information to this file.
1131
1132        :param show: if `True` then print results to console, if `False` — print only to file.
1133        :return: multi-lines string with all available broker instruments
1134        """
1135        if not self.iList:
1136            self.iList = self.Listing()
1137
1138        info = [
1139            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1140            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1141        ]
1142
1143        # add instruments count by type:
1144        for iType in self.iList.keys():
1145            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1146
1147        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1148        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1149
1150        # generating info tables with all instruments by type:
1151        for iType in self.iList.keys():
1152            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1153
1154            for instrument in self.iList[iType].keys():
1155                iName = self.iList[iType][instrument]["name"]  # instrument's name
1156                if len(iName) > 57:
1157                    iName = "{}...".format(iName[:54])  # right trim for a long string
1158
1159                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1160                    self.iList[iType][instrument]["ticker"],
1161                    iName,
1162                    self.iList[iType][instrument]["figi"],
1163                    self.iList[iType][instrument]["currency"],
1164                    self.iList[iType][instrument]["lot"],
1165                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1166                ))
1167
1168        infoText = "".join(info)
1169
1170        if show:
1171            uLogger.info(infoText)
1172
1173        if self.instrumentsFile:
1174            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1175                fH.write(infoText)
1176
1177            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1178
1179        return infoText
1180
1181    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1182        """
1183        This method search and show information about instruments by part of its ticker, FIGI or name.
1184        If `searchResultsFile` string is not empty then also save information to this file.
1185
1186        :param pattern: string with part of ticker, FIGI or instrument's name.
1187        :param show: if `True` then print results to console, if `False` — return list of result only.
1188        :return: list of dictionaries with all found instruments.
1189        """
1190        if not self.iList:
1191            self.iList = self.Listing()
1192
1193        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1194        compiledPattern = re.compile(pattern, re.IGNORECASE)
1195
1196        for iType in self.iList:
1197            for instrument in self.iList[iType].values():
1198                searchResult = compiledPattern.search(" ".join(
1199                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1200                ))
1201
1202                if searchResult:
1203                    searchResults[iType][instrument["ticker"]] = instrument
1204
1205        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1206        info = [
1207            "# Search results\n\n",
1208            "* **Search pattern:** [{}]\n".format(pattern),
1209            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1210            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1211        ]
1212        infoShort = info[:]
1213
1214        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1215        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1216        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1217
1218        if resultsLen == 0:
1219            info.append("\nNo results\n")
1220            infoShort.append("\nNo results\n")
1221            uLogger.warning("No results. Try changing your search pattern.")
1222
1223        else:
1224            for iType in searchResults:
1225                iTypeValuesCount = len(searchResults[iType].values())
1226                if iTypeValuesCount > 0:
1227                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1228                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1229
1230                    for instrument in searchResults[iType].values():
1231                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1232                            instrument["type"],
1233                            instrument["ticker"],
1234                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1235                            instrument["figi"],
1236                        ))
1237
1238                    if iTypeValuesCount <= 5:
1239                        infoShort.extend(info[-iTypeValuesCount:])
1240
1241                    else:
1242                        infoShort.extend(info[-5:])
1243                        infoShort.append(skippedLine)
1244
1245        infoText = "".join(info)
1246        infoTextShort = "".join(infoShort)
1247
1248        if show:
1249            uLogger.info(infoTextShort)
1250            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1251
1252        if self.searchResultsFile:
1253            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1254                fH.write(infoText)
1255
1256            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1257
1258        return searchResults
1259
1260    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1261        """
1262        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1263
1264        :param instruments: list of strings with tickers or FIGIs.
1265        :return: list with unique instrument FIGIs only.
1266        """
1267        requestedInstruments = []
1268        for iName in instruments:
1269            if iName not in self.aliases.keys():
1270                if iName not in requestedInstruments:
1271                    requestedInstruments.append(iName)
1272
1273            else:
1274                if iName not in requestedInstruments:
1275                    if self.aliases[iName] not in requestedInstruments:
1276                        requestedInstruments.append(self.aliases[iName])
1277
1278        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1279
1280        onlyUniqueFIGIs = []
1281        for iName in requestedInstruments:
1282            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1283                continue
1284
1285            self.ticker = iName
1286            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1287
1288            if not iData:
1289                self.ticker = ""
1290                self.figi = iName
1291
1292                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1293
1294                if not iData:
1295                    self.figi = ""
1296                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1297
1298            if iData and iData["figi"] not in onlyUniqueFIGIs:
1299                onlyUniqueFIGIs.append(iData["figi"])
1300
1301        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1302
1303        return onlyUniqueFIGIs
1304
1305    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1306        """
1307        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1308
1309        See limits: https://tinkoff.github.io/investAPI/limits/
1310
1311        If `pricesFile` string is not empty then also save information to this file.
1312
1313        :param instruments: list of strings with tickers or FIGIs.
1314        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1315        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1316                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1317        """
1318        if instruments is None or not instruments:
1319            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1320            raise Exception("Ticker or FIGI required")
1321
1322        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1323
1324        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1325
1326        iList = []  # trying to get info and current prices about all unique instruments:
1327        for self.figi in onlyUniqueFIGIs:
1328            iData = self.SearchByFIGI(requestPrice=True)
1329            iList.append(iData)
1330
1331        self.ShowListOfPrices(iList, show)
1332
1333        return iList
1334
1335    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1336        """
1337        Show table contains current prices of given instruments.
1338
1339        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1340                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1341        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1342        :return: multilines text in Markdown format as a table contains current prices.
1343        """
1344        infoText = ""
1345
1346        if show or self.pricesFile:
1347            info = [
1348                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1349                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1350                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1351            ]
1352
1353            for item in iList:
1354                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1355                    item["ticker"],
1356                    item["figi"],
1357                    item["type"],
1358                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1359                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1360                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1361                    "{} / {}".format(
1362                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1363                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1364                    ),
1365                    "{} / {}".format(
1366                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1367                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1368                    ),
1369                    item["currency"],
1370                ))
1371
1372            infoText = "".join(info)
1373
1374            if show:
1375                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1376
1377            if self.pricesFile:
1378                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1379                    fH.write(infoText)
1380
1381                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1382
1383        return infoText
1384
1385    def RequestTradingStatus(self) -> dict:
1386        """
1387        Requesting trading status for the instrument defined by `figi` variable.
1388
1389        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1390
1391        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1392
1393        :return: dictionary with trading status attributes. Response example:
1394                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1395                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1396        """
1397        if self.figi is None or not self.figi:
1398            uLogger.error("Variable `figi` must be defined for using this method!")
1399            raise Exception("FIGI required")
1400
1401        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi))
1402
1403        self.body = str({"figi": self.figi, "instrumentId": self.figi})
1404        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1405        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1406
1407        if self.moreDebug:
1408            uLogger.debug("Records about current trading status successfully received")
1409
1410        return tradingStatus
1411
1412    def RequestPortfolio(self) -> dict:
1413        """
1414        Requesting actual user's portfolio for current `accountId`.
1415
1416        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1417
1418        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1419
1420        :return: dictionary with user's portfolio.
1421        """
1422        if self.accountId is None or not self.accountId:
1423            uLogger.error("Variable `accountId` must be defined for using this method!")
1424            raise Exception("Account ID required")
1425
1426        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1427
1428        self.body = str({"accountId": self.accountId})
1429        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1430        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1431
1432        if self.moreDebug:
1433            uLogger.debug("Records about user's portfolio successfully received")
1434
1435        return rawPortfolio
1436
1437    def RequestPositions(self) -> dict:
1438        """
1439        Requesting open positions by currencies and instruments for current `accountId`.
1440
1441        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1442
1443        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1444
1445        :return: dictionary with open positions by instruments.
1446        """
1447        if self.accountId is None or not self.accountId:
1448            uLogger.error("Variable `accountId` must be defined for using this method!")
1449            raise Exception("Account ID required")
1450
1451        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1452
1453        self.body = str({"accountId": self.accountId})
1454        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1455        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1456
1457        if self.moreDebug:
1458            uLogger.debug("Records about current open positions successfully received")
1459
1460        return rawPositions
1461
1462    def RequestPendingOrders(self) -> list:
1463        """
1464        Requesting current actual pending orders for current `accountId`.
1465
1466        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1467
1468        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1469
1470        :return: list of dictionaries with pending orders.
1471        """
1472        if self.accountId is None or not self.accountId:
1473            uLogger.error("Variable `accountId` must be defined for using this method!")
1474            raise Exception("Account ID required")
1475
1476        uLogger.debug("Requesting current actual pending orders. Wait, please...")
1477
1478        self.body = str({"accountId": self.accountId})
1479        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1480        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1481
1482        uLogger.debug("[{}] records about pending orders received".format(len(rawOrders)))
1483
1484        return rawOrders
1485
1486    def RequestStopOrders(self) -> list:
1487        """
1488        Requesting current actual stop orders for current `accountId`.
1489
1490        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1491
1492        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1493
1494        :return: list of dictionaries with stop orders.
1495        """
1496        if self.accountId is None or not self.accountId:
1497            uLogger.error("Variable `accountId` must be defined for using this method!")
1498            raise Exception("Account ID required")
1499
1500        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1501
1502        self.body = str({"accountId": self.accountId})
1503        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1504        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1505
1506        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1507
1508        return rawStopOrders
1509
1510    def Overview(self, show: bool = False, details: str = "full") -> dict:
1511        """
1512        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1513        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1514        and `overviewBondsCalendarFile` are defined then also save information to file.
1515
1516        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1517        many requests about the state of the portfolio, and then, based on the received data, a large number
1518        of calculation and statistics are collected.
1519
1520        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1521        :param details: how detailed should the information be?
1522        - `full` — shows full available information about portfolio status (by default),
1523        - `positions` — shows only open positions,
1524        - `orders` — shows only sections of open limits and stop orders.
1525        - `digest` — show a short digest of the portfolio status,
1526        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1527        - `calendar` — shows only the bonds calendar section (if these present in portfolio),
1528        :return: dictionary with client's raw portfolio and some statistics.
1529        """
1530        if self.accountId is None or not self.accountId:
1531            uLogger.error("Variable `accountId` must be defined for using this method!")
1532            raise Exception("Account ID required")
1533
1534        view = {
1535            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1536                "headers": {},  # list of dictionaries, response headers without "positions" section
1537                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1538                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1539                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1540                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1541                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1542                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1543                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1544                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1545                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1546            },
1547            "stat": {  # --- some statistics calculated using "raw" sections:
1548                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1549                "availableRUB": 0.,  # available rubles (without other currencies)
1550                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1551                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1552                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1553                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1554                "sharesCostRUB": 0.,  # costs of all shares in RUB
1555                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1556                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1557                "futuresCostRUB": 0.,  # costs of all futures in RUB
1558                "Currencies": [],  # list of dictionaries of all currencies statistics
1559                "Shares": [],  # list of dictionaries of all shares statistics
1560                "Bonds": [],  # list of dictionaries of all bonds statistics
1561                "Etfs": [],  # list of dictionaries of all etfs statistics
1562                "Futures": [],  # list of dictionaries of all futures statistics
1563                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1564                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1565                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1566                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1567                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1568            },
1569            "analytics": {  # --- some analytics of portfolio:
1570                "distrByAssets": {},  # portfolio distribution by assets
1571                "distrByCompanies": {},  # portfolio distribution by companies
1572                "distrBySectors": {},  # portfolio distribution by sectors
1573                "distrByCurrencies": {},  # portfolio distribution by currencies
1574                "distrByCountries": {},  # portfolio distribution by countries
1575                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1576            }
1577        }
1578
1579        details = details.lower()
1580        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1581        if details not in availableDetails:
1582            details = "full"
1583            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1584
1585        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1586
1587        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1588        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1589        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending orders (list)
1590        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1591
1592        # save response headers without "positions" section:
1593        for key in portfolioResponse.keys():
1594            if key != "positions":
1595                view["raw"]["headers"][key] = portfolioResponse[key]
1596
1597            else:
1598                continue
1599
1600        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1601        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1602        for item in portfolioResponse["positions"]:
1603            if item["instrumentType"] == "currency":
1604                self.figi = item["figi"]
1605                curr = self.SearchByFIGI(requestPrice=False)
1606
1607                # current price of currency in RUB:
1608                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1609                    "name": curr["name"],
1610                    "currentPrice": NanoToFloat(
1611                        item["currentPrice"]["units"],
1612                        item["currentPrice"]["nano"]
1613                    ),
1614                }
1615
1616                view["raw"]["Currencies"].append(item)
1617
1618            elif item["instrumentType"] == "share":
1619                view["raw"]["Shares"].append(item)
1620
1621            elif item["instrumentType"] == "bond":
1622                view["raw"]["Bonds"].append(item)
1623
1624            elif item["instrumentType"] == "etf":
1625                view["raw"]["Etfs"].append(item)
1626
1627            elif item["instrumentType"] == "futures":
1628                view["raw"]["Futures"].append(item)
1629
1630            else:
1631                continue
1632
1633        # how many volume of currencies (by ISO currency name) are blocked:
1634        for item in view["raw"]["positions"]["blocked"]:
1635            blocked = NanoToFloat(item["units"], item["nano"])
1636            if blocked > 0:
1637                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1638
1639        # how many volume of instruments (by FIGI) are blocked:
1640        for item in view["raw"]["positions"]["securities"]:
1641            blocked = int(item["blocked"])
1642            if blocked > 0:
1643                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1644
1645        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1646
1647        if "rub" in allBlocked.keys():
1648            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1649
1650        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1651        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1652        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1653        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1654        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1655        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1656        view["stat"]["portfolioCostRUB"] = sum([
1657            view["stat"]["allCurrenciesCostRUB"],
1658            view["stat"]["sharesCostRUB"],
1659            view["stat"]["bondsCostRUB"],
1660            view["stat"]["etfsCostRUB"],
1661            view["stat"]["futuresCostRUB"],
1662        ])
1663
1664        # --- calculating some portfolio statistics:
1665        byComp = {}  # distribution by companies
1666        bySect = {}  # distribution by sectors
1667        byCurr = {}  # distribution by currencies (include RUB)
1668        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1669        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1670
1671        for item in portfolioResponse["positions"]:
1672            self.figi = item["figi"]
1673            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1674
1675            if instrument:
1676                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1677                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1678
1679                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1680                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1681
1682                else:
1683                    blocked = 0
1684
1685                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1686                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1687                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1688                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1689                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1690                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1691                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1692                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1693                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1694                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1695                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1696                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1697
1698                statData = {
1699                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1700                    "ticker": instrument["ticker"],  # ticker by FIGI
1701                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1702                    "volume": volume,  # available volume of instrument
1703                    "lots": lots,  # volume in lots of instrument
1704                    "direction": direction,  # direction of an instrument's position: short or long
1705                    "blocked": blocked,  # blocked volume of currency or instrument
1706                    "currentPrice": curPrice,  # current instrument's price in basic asset
1707                    "average": average,  # current average position price
1708                    "cost": cost,  # current cost of all volume of instrument in basic asset
1709                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1710                    "costRUB": costRUB,  # cost of instrument in ruble
1711                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1712                    "profit": profit,  # expected profit at current moment
1713                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1714                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1715                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1716                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1717                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1718                    "step": instrument["step"],  # minimum price increment
1719                }
1720
1721                # adding distribution by unique countries:
1722                if statData["country"] not in byCountry.keys():
1723                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1724
1725                else:
1726                    byCountry[statData["country"]]["cost"] += costRUB
1727                    byCountry[statData["country"]]["percent"] += percentCostRUB
1728
1729                if item["instrumentType"] != "currency":
1730                    # adding distribution by unique companies:
1731                    if statData["name"]:
1732                        if statData["name"] not in byComp.keys():
1733                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1734
1735                        else:
1736                            byComp[statData["name"]]["cost"] += costRUB
1737                            byComp[statData["name"]]["percent"] += percentCostRUB
1738
1739                    # adding distribution by unique sectors:
1740                    if statData["sector"] not in bySect.keys():
1741                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1742
1743                    else:
1744                        bySect[statData["sector"]]["cost"] += costRUB
1745                        bySect[statData["sector"]]["percent"] += percentCostRUB
1746
1747                # adding distribution by unique currencies:
1748                if currency not in byCurr.keys():
1749                    byCurr[currency] = {
1750                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1751                        "cost": costRUB,
1752                        "percent": percentCostRUB
1753                    }
1754
1755                else:
1756                    byCurr[currency]["cost"] += costRUB
1757                    byCurr[currency]["percent"] += percentCostRUB
1758
1759                # saving statistics for every instrument:
1760                if item["instrumentType"] == "currency":
1761                    view["stat"]["Currencies"].append(statData)
1762
1763                    # update dict with free funds for trading (total - blocked) by currencies
1764                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1765                    view["stat"]["funds"][currency] = {
1766                        "total": volume,
1767                        "totalCostRUB": costRUB,  # total volume cost in rubles
1768                        "free": volume - blocked,
1769                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1770                    }
1771
1772                elif item["instrumentType"] == "share":
1773                    view["stat"]["Shares"].append(statData)
1774
1775                elif item["instrumentType"] == "bond":
1776                    view["stat"]["Bonds"].append(statData)
1777
1778                elif item["instrumentType"] == "etf":
1779                    view["stat"]["Etfs"].append(statData)
1780
1781                elif item["instrumentType"] == "Futures":
1782                    view["stat"]["Futures"].append(statData)
1783
1784                else:
1785                    continue
1786
1787        # total changes in Russian Ruble:
1788        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1789        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1790        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1791        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1792        view["stat"]["funds"]["rub"] = {
1793            "total": view["stat"]["availableRUB"],
1794            "totalCostRUB": view["stat"]["availableRUB"],
1795            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1796            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1797        }
1798
1799        # --- pending orders sector data:
1800        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending orders to avoid many times price requests
1801        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1802
1803        for item in view["raw"]["orders"]:
1804            self.figi = item["figi"]
1805
1806            if item["figi"] not in uniquePendingOrdersFIGIs:
1807                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1808
1809                uniquePendingOrdersFIGIs.append(item["figi"])
1810                uniquePendingOrders[item["figi"]] = instrument
1811
1812            else:
1813                instrument = uniquePendingOrders[item["figi"]]
1814
1815            if instrument:
1816                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1817                orderType = TKS_ORDER_TYPES[item["orderType"]]
1818                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1819                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1820
1821                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1822                if item["direction"] == "ORDER_DIRECTION_BUY":
1823                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1824
1825                else:
1826                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1827
1828                # requested price for order execution:
1829                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1830
1831                # necessary changes in percent to reach target from current price:
1832                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1833
1834                view["stat"]["orders"].append({
1835                    "orderID": item["orderId"],  # orderId number parameter of current order
1836                    "figi": item["figi"],  # FIGI identification
1837                    "ticker": instrument["ticker"],  # ticker name by FIGI
1838                    "lotsRequested": item["lotsRequested"],  # requested lots value
1839                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1840                    "currentPrice": lastPrice,  # current instrument's price for defined action
1841                    "targetPrice": target,  # requested price for order execution in base currency
1842                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1843                    "percentChanges": changes,  # changes in percent to target from current price
1844                    "currency": item["currency"],  # instrument's currency name
1845                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1846                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1847                    "status": orderState,  # order status from TKS_ORDER_STATES
1848                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1849                })
1850
1851        # --- stop orders sector data:
1852        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1853        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1854
1855        for item in view["raw"]["stopOrders"]:
1856            self.figi = item["figi"]
1857
1858            if item["figi"] not in uniqueStopOrdersFIGIs:
1859                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1860
1861                uniqueStopOrdersFIGIs.append(item["figi"])
1862                uniqueStopOrders[item["figi"]] = instrument
1863
1864            else:
1865                instrument = uniqueStopOrders[item["figi"]]
1866
1867            if instrument:
1868                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1869                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1870                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1871
1872                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1873                if "expirationTime" in item.keys():
1874                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1875                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1876
1877                else:
1878                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1879                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1880
1881                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1882                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1883                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1884
1885                else:
1886                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1887
1888                # requested price when stop-order executed:
1889                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1890
1891                # price for limit-order, set up when stop-order executed:
1892                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1893
1894                # necessary changes in percent to reach target from current price:
1895                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1896
1897                view["stat"]["stopOrders"].append({
1898                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
1899                    "figi": item["figi"],  # FIGI identification
1900                    "ticker": instrument["ticker"],  # ticker name by FIGI
1901                    "lotsRequested": item["lotsRequested"],  # requested lots value
1902                    "currentPrice": lastPrice,  # current instrument's price for defined action
1903                    "targetPrice": target,  # requested price for stop-order execution in base currency
1904                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
1905                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
1906                    "percentChanges": changes,  # changes in percent to target from current price
1907                    "currency": item["currency"],  # instrument's currency name
1908                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
1909                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
1910                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
1911                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
1912                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
1913                })
1914
1915        # --- calculating data for analytics section:
1916        # portfolio distribution by assets:
1917        view["analytics"]["distrByAssets"] = {
1918            "Ruble": {
1919                "uniques": 1,
1920                "cost": view["stat"]["availableRUB"],
1921                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1922            },
1923            "Currencies": {
1924                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
1925                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
1926                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1927            },
1928            "Shares": {
1929                "uniques": len(view["stat"]["Shares"]),
1930                "cost": view["stat"]["sharesCostRUB"],
1931                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1932            },
1933            "Bonds": {
1934                "uniques": len(view["stat"]["Bonds"]),
1935                "cost": view["stat"]["bondsCostRUB"],
1936                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1937            },
1938            "Etfs": {
1939                "uniques": len(view["stat"]["Etfs"]),
1940                "cost": view["stat"]["etfsCostRUB"],
1941                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1942            },
1943            "Futures": {
1944                "uniques": len(view["stat"]["Futures"]),
1945                "cost": view["stat"]["futuresCostRUB"],
1946                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1947            },
1948        }
1949
1950        # portfolio distribution by companies:
1951        view["analytics"]["distrByCompanies"]["All money cash"] = {
1952            "ticker": "",
1953            "cost": view["stat"]["allCurrenciesCostRUB"],
1954            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1955        }
1956        view["analytics"]["distrByCompanies"].update(byComp)
1957
1958        # portfolio distribution by sectors:
1959        view["analytics"]["distrBySectors"]["All money cash"] = {
1960            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
1961            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
1962        }
1963        view["analytics"]["distrBySectors"].update(bySect)
1964
1965        # portfolio distribution by currencies:
1966        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
1967            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
1968
1969            if self.moreDebug:
1970                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
1971
1972        view["analytics"]["distrByCurrencies"].update(byCurr)
1973        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
1974        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
1975
1976        # portfolio distribution by countries:
1977        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
1978            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
1979
1980            if self.moreDebug:
1981                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
1982
1983        view["analytics"]["distrByCountries"].update(byCountry)
1984        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
1985        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
1986
1987        # --- Prepare text statistics overview in human-readable:
1988        if show:
1989            # Whatever the value `details`, header not changes:
1990            info = [
1991                "# Client's portfolio\n\n",
1992                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1993                "* **Account ID:** [{}]\n".format(self.accountId),
1994            ]
1995
1996            if details in ["full", "positions", "digest"]:
1997                info.extend([
1998                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
1999                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2000                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2001                        view["stat"]["totalChangesRUB"],
2002                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2003                        view["stat"]["totalChangesPercentRUB"],
2004                    ),
2005                ])
2006
2007            if details in ["full", "positions"]:
2008                info.extend([
2009                    "## Open positions\n\n",
2010                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2011                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2012                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2013                        "{:.2f} ({:.2f}) rub".format(
2014                            view["stat"]["availableRUB"],
2015                            view["stat"]["blockedRUB"],
2016                        )
2017                    )
2018                ])
2019
2020                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2021                    return [
2022                        "|                             |                                 |          |              |              |                     |                              |\n",
2023                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2024                            noTradeStr if noTradeStr else typeStr,
2025                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2026                        ),
2027                    ]
2028
2029                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2030                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2031                        "{} [{}]".format(data["ticker"], data["figi"]),
2032                        "{:.2f} ({:.2f}) {}".format(
2033                            data["volume"],
2034                            data["blocked"],
2035                            data["currency"],
2036                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2037                            data["volume"],
2038                            data["blocked"],
2039                        ),
2040                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2041                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2042                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2043                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2044                        "{}{:.2f} {} ({}{:.2f}%)".format(
2045                            "+" if data["profit"] > 0 else "",
2046                            data["profit"], data["baseCurrencyName"],
2047                            "+" if data["percentProfit"] > 0 else "",
2048                            data["percentProfit"],
2049                        ),
2050                    )
2051
2052                # --- Show currencies section:
2053                if view["stat"]["Currencies"]:
2054                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2055                    for item in view["stat"]["Currencies"]:
2056                        info.append(_InfoStr(item, showCurrencyName=True))
2057
2058                else:
2059                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2060
2061                # --- Show shares section:
2062                if view["stat"]["Shares"]:
2063                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2064
2065                    for item in view["stat"]["Shares"]:
2066                        info.append(_InfoStr(item))
2067
2068                else:
2069                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2070
2071                # --- Show bonds section:
2072                if view["stat"]["Bonds"]:
2073                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2074
2075                    for item in view["stat"]["Bonds"]:
2076                        info.append(_InfoStr(item))
2077
2078                else:
2079                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2080
2081                # --- Show etfs section:
2082                if view["stat"]["Etfs"]:
2083                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2084
2085                    for item in view["stat"]["Etfs"]:
2086                        info.append(_InfoStr(item))
2087
2088                else:
2089                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2090
2091                # --- Show futures section:
2092                if view["stat"]["Futures"]:
2093                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2094
2095                    for item in view["stat"]["Futures"]:
2096                        info.append(_InfoStr(item))
2097
2098                else:
2099                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2100
2101            if details in ["full", "orders"]:
2102                # --- Show pending orders section:
2103                if view["stat"]["orders"]:
2104                    info.extend([
2105                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2106                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2107                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2108                    ])
2109
2110                    for item in view["stat"]["orders"]:
2111                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2112                            "{} [{}]".format(item["ticker"], item["figi"]),
2113                            item["orderID"],
2114                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2115                            "{} {} ({}{:.2f}%)".format(
2116                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2117                                item["baseCurrencyName"],
2118                                "+" if item["percentChanges"] > 0 else "",
2119                                float(item["percentChanges"]),
2120                            ),
2121                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2122                            item["action"],
2123                            item["type"],
2124                            item["date"],
2125                        ))
2126
2127                else:
2128                    info.append("\n## Total pending limit-orders: 0\n")
2129
2130                # --- Show stop orders section:
2131                if view["stat"]["stopOrders"]:
2132                    info.extend([
2133                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2134                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2135                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2136                    ])
2137
2138                    for item in view["stat"]["stopOrders"]:
2139                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2140                            "{} [{}]".format(item["ticker"], item["figi"]),
2141                            item["orderID"],
2142                            item["lotsRequested"],
2143                            "{} {} ({}{:.2f}%)".format(
2144                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2145                                item["baseCurrencyName"],
2146                                "+" if item["percentChanges"] > 0 else "",
2147                                float(item["percentChanges"]),
2148                            ),
2149                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2150                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2151                            item["action"],
2152                            item["type"],
2153                            item["expType"],
2154                            item["createDate"],
2155                            item["expDate"],
2156                        ))
2157
2158                else:
2159                    info.append("\n## Total stop-orders: 0\n")
2160
2161            if details in ["full", "analytics"]:
2162                # -- Show analytics section:
2163                if view["stat"]["portfolioCostRUB"] > 0:
2164                    info.extend([
2165                        "\n# Analytics\n"
2166                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2167                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2168                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2169                            view["stat"]["totalChangesRUB"],
2170                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2171                            view["stat"]["totalChangesPercentRUB"],
2172                        ),
2173                        "\n## Portfolio distribution by assets\n"
2174                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2175                        "|------------------------------------|---------|---------|--------------------|\n",
2176                    ])
2177
2178                    for key in view["analytics"]["distrByAssets"].keys():
2179                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2180                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2181                                key,
2182                                view["analytics"]["distrByAssets"][key]["uniques"],
2183                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2184                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2185                            ))
2186
2187                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2188
2189                    info.extend([
2190                        "\n## Portfolio distribution by companies\n"
2191                        "\n| Company                                      | Percent | Current cost       |\n",
2192                        aSepLine,
2193                    ])
2194
2195                    for company in view["analytics"]["distrByCompanies"].keys():
2196                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2197                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2198                                "{}{}".format(
2199                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2200                                    company,
2201                                ),
2202                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2203                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2204                            ))
2205
2206                    info.extend([
2207                        "\n## Portfolio distribution by sectors\n"
2208                        "\n| Sector                                       | Percent | Current cost       |\n",
2209                        aSepLine,
2210                    ])
2211
2212                    for sector in view["analytics"]["distrBySectors"].keys():
2213                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2214                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2215                                sector,
2216                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2217                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2218                            ))
2219
2220                    info.extend([
2221                        "\n## Portfolio distribution by currencies\n"
2222                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2223                        aSepLine,
2224                    ])
2225
2226                    for curr in view["analytics"]["distrByCurrencies"].keys():
2227                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2228                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2229                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2230                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2231                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2232                            ))
2233
2234                    info.extend([
2235                        "\n## Portfolio distribution by countries\n"
2236                        "\n| Assets by country                            | Percent | Current cost       |\n",
2237                        aSepLine,
2238                    ])
2239
2240                    for country in view["analytics"]["distrByCountries"].keys():
2241                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2242                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2243                                country,
2244                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2245                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2246                            ))
2247
2248            if details in ["full", "calendar"]:
2249                # -- Show bonds payment calendar section:
2250                if view["stat"]["Bonds"]:
2251                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2252                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2253                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2254
2255                else:
2256                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2257
2258            infoText = "".join(info)
2259
2260            uLogger.info(infoText)
2261
2262            if details == "full" and self.overviewFile:
2263                filename = self.overviewFile
2264
2265            elif details == "digest" and self.overviewDigestFile:
2266                filename = self.overviewDigestFile
2267
2268            elif details == "positions" and self.overviewPositionsFile:
2269                filename = self.overviewPositionsFile
2270
2271            elif details == "orders" and self.overviewOrdersFile:
2272                filename = self.overviewOrdersFile
2273
2274            elif details == "analytics" and self.overviewAnalyticsFile:
2275                filename = self.overviewAnalyticsFile
2276
2277            elif details == "calendar" and self.overviewBondsCalendarFile:
2278                filename = self.overviewBondsCalendarFile
2279
2280            else:
2281                filename = ""
2282
2283            if filename:
2284                with open(filename, "w", encoding="UTF-8") as fH:
2285                    fH.write(infoText)
2286
2287                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2288
2289        return view
2290
2291    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2292        """
2293        Returns history operations between two given dates for current `accountId`.
2294        If `reportFile` string is not empty then also save human-readable report.
2295        Shows some statistical data of closed positions.
2296
2297        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2298        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2299        :param show: if `True` then also prints all records to the console.
2300        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2301        :return: original list of dictionaries with history of deals records from API ("operations" key):
2302                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2303                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2304        """
2305        if self.accountId is None or not self.accountId:
2306            uLogger.error("Variable `accountId` must be defined for using this method!")
2307            raise Exception("Account ID required")
2308
2309        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2310
2311        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2312
2313        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2314        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2315        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2316        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2317        customStat = {}  # custom statistics in additional to responseJSON
2318
2319        # --- output report in human-readable format:
2320        if show or self.reportFile:
2321            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2322            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2323            nextDay = ""
2324
2325            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2326
2327            if len(ops) > 0:
2328                customStat = {
2329                    "opsCount": 0,  # total operations count
2330                    "buyCount": 0,  # buy operations
2331                    "sellCount": 0,  # sell operations
2332                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2333                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2334                    "payIn": {"rub": 0.},  # Deposit brokerage account
2335                    "payOut": {"rub": 0.},  # Withdrawals
2336                    "divs": {"rub": 0.},  # Dividends income
2337                    "coupons": {"rub": 0.},  # Coupon's income
2338                    "brokerCom": {"rub": 0.},  # Service commissions
2339                    "serviceCom": {"rub": 0.},  # Service commissions
2340                    "marginCom": {"rub": 0.},  # Margin commissions
2341                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2342                }
2343
2344                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2345                for item in ops:
2346                    if item["state"] == "OPERATION_STATE_EXECUTED":
2347                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2348
2349                        # count buy operations:
2350                        if "_BUY" in item["operationType"]:
2351                            customStat["buyCount"] += 1
2352
2353                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2354                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2355
2356                            else:
2357                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2358
2359                        # count sell operations:
2360                        elif "_SELL" in item["operationType"]:
2361                            customStat["sellCount"] += 1
2362
2363                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2364                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2365
2366                            else:
2367                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2368
2369                        # count incoming operations:
2370                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2371                            if item["payment"]["currency"] in customStat["payIn"].keys():
2372                                customStat["payIn"][item["payment"]["currency"]] += payment
2373
2374                            else:
2375                                customStat["payIn"][item["payment"]["currency"]] = payment
2376
2377                        # count withdrawals operations:
2378                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2379                            if item["payment"]["currency"] in customStat["payOut"].keys():
2380                                customStat["payOut"][item["payment"]["currency"]] += payment
2381
2382                            else:
2383                                customStat["payOut"][item["payment"]["currency"]] = payment
2384
2385                        # count dividends income:
2386                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2387                            if item["payment"]["currency"] in customStat["divs"].keys():
2388                                customStat["divs"][item["payment"]["currency"]] += payment
2389
2390                            else:
2391                                customStat["divs"][item["payment"]["currency"]] = payment
2392
2393                        # count coupon's income:
2394                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2395                            if item["payment"]["currency"] in customStat["coupons"].keys():
2396                                customStat["coupons"][item["payment"]["currency"]] += payment
2397
2398                            else:
2399                                customStat["coupons"][item["payment"]["currency"]] = payment
2400
2401                        # count broker commissions:
2402                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2403                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2404                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2405
2406                            else:
2407                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2408
2409                        # count service commissions:
2410                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2411                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2412                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2413
2414                            else:
2415                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2416
2417                        # count margin commissions:
2418                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2419                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2420                                customStat["marginCom"][item["payment"]["currency"]] += payment
2421
2422                            else:
2423                                customStat["marginCom"][item["payment"]["currency"]] = payment
2424
2425                        # count withholding taxes:
2426                        elif "_TAX" in item["operationType"]:
2427                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2428                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2429
2430                            else:
2431                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2432
2433                        else:
2434                            continue
2435
2436                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2437
2438                # --- view "Actions" lines:
2439                info.extend([
2440                    "| Report sections            |                               |                              |                      |                        |\n",
2441                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2442                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2443                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2444                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2445                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2446                    ),
2447                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2448                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2449                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2450                    ),
2451                ])
2452
2453                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2454                for key in opsKeys:
2455                    if key == "rub":
2456                        continue
2457
2458                    info.extend([
2459                        "|                            |                               | {:<28} |                      |                        |\n".format(
2460                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2461                        ),
2462                        "|                            |                               | {:<28} |                      |                        |\n".format(
2463                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2464                        ),
2465                    ])
2466
2467                info.append(splitLine1)
2468
2469                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2470                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2471                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2472                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2473                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2474                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2475                    )
2476
2477                # --- view "Payments" lines:
2478                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2479                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2480
2481                for key in paymentsKeys:
2482                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2483
2484                info.append(splitLine1)
2485
2486                # --- view "Commissions and taxes" lines:
2487                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2488                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2489
2490                for key in comKeys:
2491                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2492
2493                info.append(splitLine1)
2494
2495                info.extend([
2496                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2497                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2498                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2499                ])
2500
2501            else:
2502                info.append("Broker returned no operations during this period\n")
2503
2504            # --- view "Operations" section:
2505            for item in ops:
2506                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2507                    continue
2508
2509                else:
2510                    self.figi = item["figi"] if item["figi"] else ""
2511                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2512                    instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {}
2513
2514                    # group of deals during one day:
2515                    if nextDay and item["date"].split("T")[0] != nextDay:
2516                        info.append(splitLine2)
2517                        nextDay = ""
2518
2519                    else:
2520                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2521
2522                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2523                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2524                        self.figi if self.figi else "—",
2525                        instrument["ticker"] if instrument else "—",
2526                        instrument["type"] if instrument else "—",
2527                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2528                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2529                        TKS_OPERATION_STATES[item["state"]],
2530                        TKS_OPERATION_TYPES[item["operationType"]],
2531                    ))
2532
2533            infoText = "".join(info)
2534
2535            if show:
2536                if self.moreDebug:
2537                    uLogger.debug("Records about history of a client's operations successfully received")
2538
2539                uLogger.info(infoText)
2540
2541            if self.reportFile:
2542                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2543                    fH.write(infoText)
2544
2545                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2546
2547        return ops, customStat
2548
2549    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2550        """
2551        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2552
2553        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2554        Warning! Broker server used ISO UTC time by default.
2555
2556        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2557        Also, `historyFile` used to update history with `onlyMissing` parameter.
2558
2559        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2560
2561        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2562        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2563        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2564                         `"hour"`, `"day"`. Default: `"hour"`.
2565        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2566                            False by default. Warning! History appends only from last candle to current time
2567                            with always update last candle!
2568        :param csvSep: separator if csv-file is used, `,` by default.
2569        :param show: if `True` then also prints Pandas DataFrame to the console.
2570        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2571                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2572        """
2573        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2574        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2575        history = None  # empty pandas object for history
2576
2577        if interval not in TKS_CANDLE_INTERVALS.keys():
2578            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2579            raise Exception("Incorrect value")
2580
2581        if not (self.ticker or self.figi):
2582            uLogger.error("Ticker or FIGI must be defined!")
2583            raise Exception("Ticker or FIGI required")
2584
2585        if self.ticker and not self.figi:
2586            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2587            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2588
2589        if self.figi and not self.ticker:
2590            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2591            self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2592
2593        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2594        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2595        if interval.lower() != "day":
2596            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2597
2598        delta = dtEnd - dtStart  # current UTC time minus last time in file
2599        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2600
2601        # calculate history length in candles:
2602        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2603        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2604            length += 1  # to avoid fraction time
2605
2606        # calculate data blocks count:
2607        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2608
2609        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2610        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2611        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2612        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2613        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi))
2614
2615        tempOld = None  # pandas object for old history, if --only-missing key present
2616        lastTime = None  # datetime object of last old candle in file
2617
2618        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2619            uLogger.debug("--only-missing key present, add only last missing candles...")
2620            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2621
2622            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2623
2624            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2625            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2626            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2627            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2628
2629            # get last datetime object from last string in file or minus 1 delta if file is empty:
2630            if len(tempOld) > 0:
2631                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2632
2633            else:
2634                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2635
2636            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2637
2638        responseJSONs = []  # raw history blocks of data
2639
2640        blockEnd = dtEnd
2641        for item in range(blocks):
2642            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2643            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2644
2645            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2646                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2647            ))
2648
2649            if blockStart == blockEnd:
2650                uLogger.debug("Skipped this zero-length block...")
2651
2652            else:
2653                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2654                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2655                self.body = str({
2656                    "figi": self.figi,
2657                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2658                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2659                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2660                })
2661                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2662
2663                if "code" in responseJSON.keys():
2664                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2665
2666                else:
2667                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2668                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2669
2670                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2671
2672            blockEnd = blockStart
2673
2674        printCount = len(responseJSONs)  # candles to show in console
2675        if responseJSONs:
2676            tempHistory = pd.DataFrame(
2677                data={
2678                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2679                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2680                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2681                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2682                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2683                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2684                    "volume": [int(item["volume"]) for item in responseJSONs],
2685                },
2686                index=range(len(responseJSONs)),
2687                columns=["date", "time", "open", "high", "low", "close", "volume"],
2688            )
2689            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2690            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2691
2692            # append only newest candles to old history if --only-missing key present:
2693            if onlyMissing and tempOld is not None and lastTime is not None:
2694                index = 0  # find start index in tempHistory data:
2695
2696                for i, item in tempHistory.iterrows():
2697                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2698
2699                    if curTime == lastTime:
2700                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2701                        index = i
2702                        printCount = index + 1
2703                        break
2704
2705                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2706
2707            else:
2708                history = tempHistory  # if no `--only-missing` key then load full data from server
2709
2710            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2711
2712        if history is not None and not history.empty:
2713            if show:
2714                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2715                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2716                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2717                ))
2718
2719        else:
2720            uLogger.warning("Received an empty candles history!")
2721
2722        if self.historyFile is not None:
2723            if history is not None and not history.empty:
2724                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2725                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile)))
2726
2727            else:
2728                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2729
2730        else:
2731            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2732
2733        return history
2734
2735    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2736        """
2737        Load candles history from csv-file and return Pandas DataFrame object.
2738
2739        See also: `History()` and `ShowHistoryChart()` methods.
2740
2741        :param filePath: path to csv-file to open.
2742        """
2743        loadedHistory = None  # init candles data object
2744
2745        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2746
2747        if os.path.exists(filePath):
2748            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2749
2750            tfStr = self.priceModel.FormattedDelta(
2751                self.priceModel.timeframe,
2752                "{days} days {hours}h {minutes}m {seconds}s",
2753            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2754                self.priceModel.timeframe,
2755                "{hours}h {minutes}m {seconds}s",
2756            )
2757
2758            if loadedHistory is not None and not loadedHistory.empty:
2759                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2760                    len(loadedHistory),
2761                    tfStr,
2762                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2763                )
2764
2765            else:
2766                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2767
2768        else:
2769            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2770
2771        return loadedHistory
2772
2773    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2774        """
2775        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2776
2777        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2778        Default: `index.html` (both for interact and non-interact candlesticks chart).
2779
2780        See also: `History()` and `LoadHistory()` methods.
2781
2782        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2783        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2784                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2785                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2786                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2787        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2788                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2789        """
2790        if isinstance(candles, str):
2791            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2792            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2793
2794        elif isinstance(candles, pd.DataFrame):
2795            self.priceModel.prices = candles  # set candles chain from variable
2796            self.priceModel.ticker = self.ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2797
2798            if "datetime" not in candles.columns:
2799                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2800
2801        else:
2802            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2803            raise Exception("Incorrect value")
2804
2805        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2806
2807        if interact:
2808            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2809
2810            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2811
2812        else:
2813            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2814
2815            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2816
2817        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2818
2819    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2820        """
2821        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2822        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2823
2824        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2825
2826        :param operation: string "Buy" or "Sell".
2827        :param lots: volume, integer count of lots >= 1.
2828        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2829        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2830        :param expDate: string "Undefined" by default or local date in future,
2831                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2832        :return: JSON with response from broker server.
2833        """
2834        if self.accountId is None or not self.accountId:
2835            uLogger.error("Variable `accountId` must be defined for using this method!")
2836            raise Exception("Account ID required")
2837
2838        if operation is None or not operation or operation not in ("Buy", "Sell"):
2839            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2840            raise Exception("Incorrect value")
2841
2842        if lots is None or lots < 1:
2843            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2844            lots = 1
2845
2846        if tp is None or tp < 0:
2847            tp = 0
2848
2849        if sl is None or sl < 0:
2850            sl = 0
2851
2852        if expDate is None or not expDate:
2853            expDate = "Undefined"
2854
2855        if not (self.ticker or self.figi):
2856            uLogger.error("Ticker or FIGI must be defined!")
2857            raise Exception("Ticker or FIGI required")
2858
2859        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
2860        self.ticker = instrument["ticker"]
2861        self.figi = instrument["figi"]
2862
2863        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate))
2864
2865        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2866        self.body = str({
2867            "figi": self.figi,
2868            "quantity": str(lots),
2869            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2870            "accountId": str(self.accountId),
2871            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2872        })
2873        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
2874
2875        if "orderId" in response.keys():
2876            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2877                operation, response["orderId"],
2878                self.ticker, self.figi, lots,
2879                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2880                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2881                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2882            ))
2883
2884            if tp > 0:
2885                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2886
2887            if sl > 0:
2888                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2889
2890        else:
2891            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.")
2892
2893        return response
2894
2895    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2896        """
2897        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
2898        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
2899
2900        See also: `Order()` and `Trade()` docstrings.
2901
2902        :param lots: volume, integer count of lots >= 1.
2903        :param tp: float > 0, take profit price of stop-order.
2904        :param sl: float > 0, stop loss price of stop-order.
2905        :param expDate: it's a local date in future.
2906                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2907        :return: JSON with response from broker server.
2908        """
2909        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
2910
2911    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2912        """
2913        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
2914        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2915
2916        See also: `Order()` and `Trade()` docstrings.
2917
2918        :param lots: volume, integer count of lots >= 1.
2919        :param tp: float > 0, take profit price of stop-order.
2920        :param sl: float > 0, stop loss price of stop-order.
2921        :param expDate: it's a local date in the future.
2922                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2923        :return: JSON with response from broker server.
2924        """
2925        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
2926
2927    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
2928        """
2929        Close position of given instruments.
2930
2931        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
2932        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
2933                         This avoids unnecessary downloading data from the server.
2934        """
2935        if instruments is None or not instruments:
2936            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
2937            raise Exception("Ticker or FIGI required")
2938
2939        if isinstance(instruments, str):
2940            instruments = [instruments]
2941
2942        uniqueInstruments = self.GetUniqueFIGIs(instruments)
2943        if uniqueInstruments:
2944            if portfolio is None or not portfolio:
2945                portfolio = self.Overview(show=False)
2946
2947            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
2948            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
2949
2950            for self.figi in uniqueInstruments:
2951                if self.figi not in allOpened:
2952                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi))
2953                    continue
2954
2955                # search open trade info about instrument by ticker:
2956                instrument = {}
2957                for iType in TKS_INSTRUMENTS:
2958                    if instrument:
2959                        break
2960
2961                    for item in portfolio["stat"][iType]:
2962                        if item["figi"] == self.figi:
2963                            instrument = item
2964                            break
2965
2966                if instrument:
2967                    self.ticker = instrument["ticker"]
2968                    self.figi = instrument["figi"]
2969
2970                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
2971                        self.ticker,
2972                        self.figi,
2973                        int(instrument["volume"]),
2974                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
2975                    ))
2976
2977                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
2978
2979                    if tradeLots > 0:
2980                        if instrument["blocked"] > 0:
2981                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
2982                                instrument["blocked"],
2983                                self.ticker,
2984                                tradeLots,
2985                            ))
2986
2987                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
2988                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
2989
2990                    else:
2991                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
2992
2993    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
2994        """
2995        Close all positions of given instruments with defined type.
2996
2997        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
2998        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
2999                         This avoids unnecessary downloading data from the server.
3000        """
3001        if iType not in TKS_INSTRUMENTS:
3002            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3003
3004        else:
3005            if portfolio is None or not portfolio:
3006                portfolio = self.Overview(show=False)
3007
3008            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3009            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3010
3011            if tickers and portfolio:
3012                self.CloseTrades(tickers, portfolio)
3013
3014            else:
3015                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3016
3017    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3018        """
3019        Universal method to create market or limit orders with all available parameters for current `accountId`.
3020        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3021
3022        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3023        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3024
3025        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3026        then broker immediately open market order as you can do simple --buy or --sell operations!
3027
3028        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3029        When current price will go up or down to target price value then broker opens a limit order.
3030        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3031
3032        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3033
3034        :param operation: string "Buy" or "Sell".
3035        :param orderType: string "Limit" or "Stop".
3036        :param lots: volume, integer count of lots >= 1.
3037        :param targetPrice: target price > 0. This is open trade price for limit order.
3038        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3039                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3040        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3041                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3042                         Stop loss order always executed by market price.
3043        :param expDate: string "Undefined" by default or local date in future.
3044                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3045                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3046                        A limit order has no expiration date, it lasts until the end of the trading day.
3047        :return: JSON with response from broker server.
3048        """
3049        if self.accountId is None or not self.accountId:
3050            uLogger.error("Variable `accountId` must be defined for using this method!")
3051            raise Exception("Account ID required")
3052
3053        if operation is None or not operation or operation not in ("Buy", "Sell"):
3054            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3055            raise Exception("Incorrect value")
3056
3057        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3058            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3059            raise Exception("Incorrect value")
3060
3061        if lots is None or lots < 1:
3062            uLogger.error("You must define trade volume > 0: integer count of lots!")
3063            raise Exception("Incorrect value")
3064
3065        if targetPrice is None or targetPrice <= 0:
3066            uLogger.error("Target price for limit-order must be greater than 0!")
3067            raise Exception("Incorrect value")
3068
3069        if limitPrice is None or limitPrice <= 0:
3070            limitPrice = targetPrice
3071
3072        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3073            stopType = "Limit"
3074
3075        if expDate is None or not expDate:
3076            expDate = "Undefined"
3077
3078        if not (self.ticker or self.figi):
3079            uLogger.error("Tocker or FIGI must be defined!")
3080            raise Exception("Ticker or FIGI required")
3081
3082        response = {}
3083        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
3084        self.ticker = instrument["ticker"]
3085        self.figi = instrument["figi"]
3086
3087        if orderType == "Limit":
3088            uLogger.debug(
3089                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3090                    self.ticker, self.figi,
3091                    operation, lots, targetPrice, instrument["currency"],
3092                ))
3093
3094            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3095            self.body = str({
3096                "figi": self.figi,
3097                "quantity": str(lots),
3098                "price": FloatToNano(targetPrice),
3099                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3100                "accountId": str(self.accountId),
3101                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3102            })
3103            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3104
3105            if "orderId" in response.keys():
3106                uLogger.info(
3107                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3108                        response["orderId"],
3109                        self.ticker, self.figi,
3110                        operation, lots, targetPrice, instrument["currency"],
3111                    ))
3112
3113                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3114                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3115                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3116                            targetPrice, instrument["currency"],
3117                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3118                        ))
3119
3120                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3121                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3122                            targetPrice, instrument["currency"],
3123                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3124                        ))
3125
3126            else:
3127                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.")
3128
3129        if orderType == "Stop":
3130            uLogger.debug(
3131                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3132                    self.ticker, self.figi,
3133                    operation, lots,
3134                    targetPrice, instrument["currency"],
3135                    limitPrice, instrument["currency"],
3136                    stopType, expDate,
3137                ))
3138
3139            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3140            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3141            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3142
3143            body = {
3144                "figi": self.figi,
3145                "quantity": str(lots),
3146                "price": FloatToNano(limitPrice),
3147                "stopPrice": FloatToNano(targetPrice),
3148                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3149                "accountId": str(self.accountId),
3150                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3151                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3152            }
3153
3154            if expDateUTC:
3155                body["expireDate"] = expDateUTC
3156
3157            self.body = str(body)
3158            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3159
3160            if "stopOrderId" in response.keys():
3161                uLogger.info(
3162                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3163                        response["stopOrderId"],
3164                        self.ticker, self.figi,
3165                        operation, lots,
3166                        targetPrice, instrument["currency"],
3167                        limitPrice, instrument["currency"],
3168                        TKS_STOP_ORDER_TYPES[stopOrderType],
3169                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3170                    ))
3171
3172                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3173                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3174                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3175                            targetPrice, instrument["currency"],
3176                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3177                        ))
3178
3179                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3180                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3181                            targetPrice, instrument["currency"],
3182                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3183                        ))
3184
3185            else:
3186                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.")
3187
3188        return response
3189
3190    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3191        """
3192        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3193        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3194        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3195        See also: `Order()` docstring.
3196
3197        :param lots: volume, integer count of lots >= 1.
3198        :param targetPrice: target price > 0. This is open trade price for limit order.
3199        :return: JSON with response from broker server.
3200        """
3201        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3202
3203    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3204        """
3205        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3206        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3207        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3208        target price value then broker opens a limit order. See also: `Order()` docstring.
3209
3210        :param lots: volume, integer count of lots >= 1.
3211        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3212        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3213                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3214        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3215                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3216        :param expDate: string "Undefined" by default or local date in future.
3217                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3218                        This date is converting to UTC format for server.
3219        :return: JSON with response from broker server.
3220        """
3221        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3222
3223    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3224        """
3225        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3226        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3227        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3228        See also: `Order()` docstring.
3229
3230        :param lots: volume, integer count of lots >= 1.
3231        :param targetPrice: target price > 0. This is open trade price for limit order.
3232        :return: JSON with response from broker server.
3233        """
3234        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3235
3236    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3237        """
3238        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3239        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3240        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3241        target price value then broker opens a limit order. See also: `Order()` docstring.
3242
3243        :param lots: volume, integer count of lots >= 1.
3244        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3245        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3246                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3247        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3248                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3249        :param expDate: string "Undefined" by default or local date in future.
3250                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3251                        This date is converting to UTC format for server.
3252        :return: JSON with response from broker server.
3253        """
3254        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3255
3256    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3257        """
3258        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3259
3260        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3261        :param allOrdersIDs: pre-received lists of all active pending orders.
3262                             This avoids unnecessary downloading data from the server.
3263        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3264        """
3265        if self.accountId is None or not self.accountId:
3266            uLogger.error("Variable `accountId` must be defined for using this method!")
3267            raise Exception("Account ID required")
3268
3269        if orderIDs:
3270            if allOrdersIDs is None or not allOrdersIDs:
3271                rawOrders = self.RequestPendingOrders()
3272                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3273
3274            if allStopOrdersIDs is None or not allStopOrdersIDs:
3275                rawStopOrders = self.RequestStopOrders()
3276                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3277
3278            for orderID in orderIDs:
3279                idInPendingOrders = orderID in allOrdersIDs
3280                idInStopOrders = orderID in allStopOrdersIDs
3281
3282                if not (idInPendingOrders or idInStopOrders):
3283                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3284                    continue
3285
3286                else:
3287                    if idInPendingOrders:
3288                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3289
3290                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3291                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3292                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3293                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3294
3295                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3296                            if self.moreDebug:
3297                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3298
3299                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3300
3301                        else:
3302                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3303
3304                    elif idInStopOrders:
3305                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3306
3307                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3308                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3309                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3310                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3311
3312                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3313                            if self.moreDebug:
3314                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3315
3316                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3317
3318                        else:
3319                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3320
3321                    else:
3322                        continue
3323
3324    def CloseAllOrders(self) -> None:
3325        """
3326        Gets a list of open pending and stop orders and cancel it all.
3327        """
3328        rawOrders = self.RequestPendingOrders()
3329        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3330        lenOrders = len(allOrdersIDs)
3331
3332        rawStopOrders = self.RequestStopOrders()
3333        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3334        lenSOrders = len(allStopOrdersIDs)
3335
3336        if lenOrders > 0 or lenSOrders > 0:
3337            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3338
3339            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3340
3341        else:
3342            uLogger.info("Orders not found, nothing to cancel.")
3343
3344    def CloseAll(self, *args) -> None:
3345        """
3346        Close all available (not blocked) opened trades and orders.
3347
3348        Also, you can select one or more keywords case-insensitive:
3349        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3350
3351        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3352        """
3353        overview = self.Overview(show=False)  # get all open trades info
3354
3355        if len(args) == 0:
3356            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3357            self.CloseAllOrders()  # close all pending and stop orders
3358
3359            for iType in TKS_INSTRUMENTS:
3360                if iType != "Currencies":
3361                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3362
3363        else:
3364            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3365            lowerArgs = [x.lower() for x in args]
3366
3367            if "orders" in lowerArgs:
3368                self.CloseAllOrders()  # close all pending and stop orders
3369
3370            for iType in TKS_INSTRUMENTS:
3371                if iType.lower() in lowerArgs and iType != "Currencies":
3372                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3373
3374    @staticmethod
3375    def ParseOrderParameters(operation, **inputParameters):
3376        """
3377        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3378
3379        :param operation: string "Buy" or "Sell".
3380        :param inputParameters: this is dict of strings that looks like this
3381               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3382               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3383               "prices" key: one or more prices to open limit-orders
3384               Counts of values in lots and prices lists must be equals!
3385        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3386        """
3387        # TODO: update order grid work with api v2
3388        pass
3389        # uLogger.debug("Input parameters: {}".format(inputParameters))
3390        #
3391        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3392        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3393        #     raise Exception("Incorrect value")
3394        #
3395        # if "l" in inputParameters.keys():
3396        #     inputParameters["lots"] = inputParameters.pop("l")
3397        #
3398        # if "p" in inputParameters.keys():
3399        #     inputParameters["prices"] = inputParameters.pop("p")
3400        #
3401        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3402        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3403        #     raise Exception("Incorrect value")
3404        #
3405        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3406        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3407        #
3408        # if len(lots) != len(prices):
3409        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3410        #     raise Exception("Incorrect value")
3411        #
3412        # uLogger.debug("Extracted parameters for orders:")
3413        # uLogger.debug("lots = {}".format(lots))
3414        # uLogger.debug("prices = {}".format(prices))
3415        #
3416        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3417        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3418        # uLogger.debug("Order parameters: {}".format(result))
3419        #
3420        # return result
3421
3422    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3423        """
3424        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3425
3426        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3427        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3428        """
3429        result = False
3430        msg = "Instrument not defined!"
3431
3432        if portfolio is None or not portfolio:
3433            portfolio = self.Overview(show=False)
3434
3435        if self.ticker:
3436            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3437            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3438
3439            for iType in TKS_INSTRUMENTS:
3440                for instrument in portfolio["stat"][iType]:
3441                    if instrument["ticker"] == self.ticker:
3442                        result = True
3443                        msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker)
3444                        break
3445
3446        elif self.figi:
3447            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3448            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3449
3450            for iType in TKS_INSTRUMENTS:
3451                for instrument in portfolio["stat"][iType]:
3452                    if instrument["figi"] == self.figi:
3453                        result = True
3454                        msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi)
3455                        break
3456
3457        else:
3458            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3459
3460        uLogger.debug(msg)
3461
3462        return result
3463
3464    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3465        """
3466        Returns instrument from the user's portfolio if it presents there.
3467        Instrument must be defined by `ticker` (highly priority) or `figi`.
3468
3469        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3470        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3471        """
3472        result = None
3473        msg = "Instrument not defined!"
3474
3475        if portfolio is None or not portfolio:
3476            portfolio = self.Overview(show=False)
3477
3478        if self.ticker:
3479            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self.ticker))
3480            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3481
3482            for iType in TKS_INSTRUMENTS:
3483                for instrument in portfolio["stat"][iType]:
3484                    if instrument["ticker"] == self.ticker:
3485                        result = instrument
3486                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"])
3487                        break
3488
3489        elif self.figi:
3490            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3491            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3492
3493            for iType in TKS_INSTRUMENTS:
3494                for instrument in portfolio["stat"][iType]:
3495                    if instrument["figi"] == self.figi:
3496                        result = instrument
3497                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi)
3498                        break
3499
3500        else:
3501            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3502
3503        uLogger.debug(msg)
3504
3505        return result
3506
3507    def RequestLimits(self) -> dict:
3508        """
3509        Method for obtaining the available funds for withdrawal for current `accountId`.
3510
3511        See also:
3512        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3513        - `OverviewLimits()` method
3514
3515        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3516                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3517                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3518                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3519        """
3520        if self.accountId is None or not self.accountId:
3521            uLogger.error("Variable `accountId` must be defined for using this method!")
3522            raise Exception("Account ID required")
3523
3524        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3525
3526        self.body = str({"accountId": self.accountId})
3527        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3528        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3529
3530        if self.moreDebug:
3531            uLogger.debug("Records about available funds for withdrawal successfully received")
3532
3533        return rawLimits
3534
3535    def OverviewLimits(self, show: bool = False) -> dict:
3536        """
3537        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3538
3539        See also: `RequestLimits()`.
3540
3541        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3542        :return: dict with raw parsed data from server and some calculated statistics about it.
3543        """
3544        if self.accountId is None or not self.accountId:
3545            uLogger.error("Variable `accountId` must be defined for using this method!")
3546            raise Exception("Account ID required")
3547
3548        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3549
3550        view = {
3551            "rawLimits": rawLimits,
3552            "limits": {  # parsed data for every currency:
3553                "money": {  # this is an array of portfolio currency positions
3554                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3555                },
3556                "blocked": {  # this is an array of blocked currency
3557                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3558                },
3559                "blockedGuarantee": {  # this is locked money under collateral for futures
3560                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3561                },
3562            },
3563        }
3564
3565        # --- Prepare text table with limits in human-readable format:
3566        if show:
3567            info = [
3568                "# Withdrawal limits\n\n",
3569                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3570                "* **Account ID:** [{}]\n".format(self.accountId),
3571            ]
3572
3573            if view["limits"]["money"]:
3574                info.extend([
3575                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3576                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3577                ])
3578
3579            else:
3580                info.append("\nNo withdrawal limits\n")
3581
3582            for curr in view["limits"]["money"].keys():
3583                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3584                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3585                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3586
3587                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3588                    "[{}]".format(curr),
3589                    "{:.2f}".format(view["limits"]["money"][curr]),
3590                    "{:.2f}".format(availableMoney),
3591                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3592                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3593                )
3594
3595                if curr == "rub":
3596                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3597
3598                else:
3599                    info.append(infoStr)
3600
3601            infoText = "".join(info)
3602
3603            uLogger.info(infoText)
3604
3605            if self.withdrawalLimitsFile:
3606                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3607                    fH.write(infoText)
3608
3609                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3610
3611        return view
3612
3613    def RequestAccounts(self) -> dict:
3614        """
3615        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3616
3617        See also:
3618        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3619        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3620        - `OverviewUserInfo()` method
3621
3622        :return: dict with raw data from server that contains accounts info. Example of dict:
3623                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3624                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3625                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3626                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3627        """
3628        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3629
3630        self.body = str({})
3631        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3632        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3633
3634        if self.moreDebug:
3635            uLogger.debug("Records about available accounts successfully received")
3636
3637        return rawAccounts
3638
3639    def RequestUserInfo(self) -> dict:
3640        """
3641        Method for requesting common user's information.
3642
3643        See also:
3644        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3645        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3646        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3647        - `OverviewUserInfo()` method
3648
3649        :return: dict with raw data from server that contains user's information. Example of dict:
3650                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3651                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3652        """
3653        uLogger.debug("Requesting common user's information. Wait, please...")
3654
3655        self.body = str({})
3656        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3657        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3658
3659        if self.moreDebug:
3660            uLogger.debug("Records about current user successfully received")
3661
3662        return rawUserInfo
3663
3664    def RequestMarginStatus(self, accountId: str = None) -> dict:
3665        """
3666        Method for requesting margin calculation for defined account ID.
3667
3668        See also:
3669        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3670        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3671        - `OverviewUserInfo()` method
3672
3673        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3674        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3675                 Example of responses:
3676                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3677                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3678                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3679                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3680                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3681                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3682        """
3683        if accountId is None or not accountId:
3684            if self.accountId is None or not self.accountId:
3685                uLogger.error("Variable `accountId` must be defined for using this method!")
3686                raise Exception("Account ID required")
3687
3688            else:
3689                accountId = self.accountId  # use `self.accountId` (main ID) by default
3690
3691        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3692
3693        self.body = str({"accountId": accountId})
3694        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3695        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3696
3697        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3698            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3699            rawMargin = {}
3700
3701        else:
3702            if self.moreDebug:
3703                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3704
3705        return rawMargin
3706
3707    def RequestTariffLimits(self) -> dict:
3708        """
3709        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
3710
3711        See also:
3712        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
3713        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
3714        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
3715        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
3716        - `OverviewUserInfo()` method
3717
3718        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
3719                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
3720                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
3721        """
3722        uLogger.debug("Requesting limits of current tariff. Wait, please...")
3723
3724        self.body = str({})
3725        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
3726        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3727
3728        if self.moreDebug:
3729            uLogger.debug("Records with limits of current tariff successfully received")
3730
3731        return rawTariffLimits
3732
3733    def RequestBondCoupons(self, iJSON: dict) -> dict:
3734        """
3735        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
3736        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
3737        All dates are in UTC timezone.
3738
3739        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
3740        Documentation:
3741        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
3742        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
3743
3744        See also: `ExtendBondsData()`.
3745
3746        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]`
3747                      If raw iJSON is not data of bond then server returns an error [400] with message:
3748                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
3749        :return: dictionary with bond payment calendar. Response example
3750                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
3751                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
3752                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
3753                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
3754        """
3755        if iJSON["figi"] is None or not iJSON["figi"]:
3756            uLogger.error("FIGI must be defined for using this method!")
3757            raise Exception("FIGI required")
3758
3759        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
3760        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
3761
3762        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
3763            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
3764            self.figi,
3765            startDate,
3766            endDate,
3767        ))
3768
3769        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
3770        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
3771        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
3772
3773        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
3774            uLogger.warning("Instrument type is not bond!")
3775
3776        else:
3777            if self.moreDebug:
3778                uLogger.debug("Records about bond payment calendar successfully received")
3779
3780        return calendar
3781
3782    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
3783        """
3784        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
3785        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
3786        coupon yields, current yields and some statistics etc.
3787
3788        WARNING! This is too long operation if a lot of bonds requested from broker server.
3789
3790        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
3791
3792        :param instruments: list of strings with tickers or FIGIs.
3793        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
3794                     for further used by data scientists or stock analytics.
3795        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
3796                 In XLSX-file and Pandas DataFrame fields mean:
3797                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
3798                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
3799        """
3800        if instruments is None or not instruments:
3801            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3802            raise Exception("Ticker or FIGI required")
3803
3804        if isinstance(instruments, str):
3805            instruments = [instruments]
3806
3807        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3808
3809        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
3810
3811        iCount = len(uniqueInstruments)
3812        tooLong = iCount >= 20
3813        if tooLong:
3814            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
3815
3816        bonds = None
3817        for i, self.figi in enumerate(uniqueInstruments):
3818            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
3819
3820            if "type" in instrument.keys() and instrument["type"] == "Bonds":
3821                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
3822                rawBond = self.SearchByFIGI(requestPrice=True)
3823
3824                # Widen raw data with UTC current time (iData["actualDateTime"]):
3825                actualDate = datetime.now(tzutc())
3826                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
3827
3828                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
3829                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
3830
3831                # Replace some values with human-readable:
3832                iData["nominalCurrency"] = iData["nominal"]["currency"]
3833                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
3834                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
3835                iData["aciCurrency"] = iData["aciValue"]["currency"]
3836                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
3837                iData["issueSize"] = int(iData["issueSize"])
3838                iData["issueSizePlan"] = int(iData["issueSizePlan"])
3839                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
3840                iData["step"] = iData["step"] if "step" in iData.keys() else 0
3841                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
3842                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
3843                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
3844                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
3845                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
3846                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
3847                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
3848
3849                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
3850                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
3851                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
3852                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
3853                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
3854                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
3855                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
3856                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
3857                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
3858                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
3859                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
3860
3861                # Widen raw data with calendar data from `rawCalendar` values:
3862                calendarData = []
3863                if "events" in iData["rawCalendar"].keys():
3864                    for item in iData["rawCalendar"]["events"]:
3865                        calendarData.append({
3866                            "couponDate": item["couponDate"],
3867                            "couponNumber": int(item["couponNumber"]),
3868                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
3869                            "payCurrency": item["payOneBond"]["currency"],
3870                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
3871                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
3872                            "couponStartDate": item["couponStartDate"],
3873                            "couponEndDate": item["couponEndDate"],
3874                            "couponPeriod": item["couponPeriod"],
3875                        })
3876
3877                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
3878                    if "maturityDate" not in iData.keys():
3879                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
3880
3881                # Widen raw data with Coupon Rate.
3882                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
3883                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
3884                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
3885                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
3886
3887                # Widen raw data with Yield to Maturity (YTM) on current date.
3888                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
3889                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
3890                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
3891                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
3892                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
3893                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
3894
3895                iData["calendar"] = calendarData  # adds calendar at the end
3896
3897                # Remove not used data:
3898                iData.pop("uid")
3899                iData.pop("positionUid")
3900                iData.pop("currentPrice")
3901                iData.pop("rawCalendar")
3902
3903                colNames = list(iData.keys())
3904                if bonds is None:
3905                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
3906
3907                else:
3908                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
3909
3910            else:
3911                uLogger.warning("Instrument is not a bond!")
3912
3913            processed = round(100 * (i + 1) / iCount, 1)
3914            if tooLong and processed % 5 == 0:
3915                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
3916
3917            else:
3918                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
3919
3920        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
3921
3922        # Saving bonds from Pandas DataFrame to XLSX sheet:
3923        if xlsx and self.bondsXLSXFile:
3924            with pd.ExcelWriter(
3925                    path=self.bondsXLSXFile,
3926                    date_format=TKS_DATE_FORMAT,
3927                    datetime_format=TKS_DATE_TIME_FORMAT,
3928                    mode="w",
3929            ) as writer:
3930                bonds.to_excel(
3931                    writer,
3932                    sheet_name="Extended bonds data",
3933                    index=True,
3934                    encoding="UTF-8",
3935                    freeze_panes=(1, 1),
3936                )  # saving as XLSX-file with freeze first row and column as headers
3937
3938            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
3939
3940        return bonds
3941
3942    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
3943        """
3944        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
3945
3946        WARNING! This is too long operation if a lot of bonds requested from broker server.
3947
3948        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
3949
3950        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
3951                        extended information about bonds: main info, current prices, bond payment calendar,
3952                        coupon yields, current yields and some statistics etc.
3953                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
3954        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
3955                     for further used by data scientists or stock analytics.
3956        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
3957        """
3958        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
3959            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
3960
3961        uLogger.debug("Generating bond payments calendar data. Wait, please...")
3962
3963        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
3964        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
3965        calendar = None
3966        for bond in extBonds.iterrows():
3967            for item in bond[1]["calendar"]:
3968                cData = {
3969                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
3970                    "couponDate": item["couponDate"],
3971                    "figi": bond[1]["figi"],
3972                    "ticker": bond[1]["ticker"],
3973                    "name": bond[1]["name"],
3974                    "couponNumber": item["couponNumber"],
3975                    "payOneBond": item["payOneBond"],
3976                    "payCurrency": item["payCurrency"],
3977                    "couponType": item["couponType"],
3978                    "couponPeriod": item["couponPeriod"],
3979                    "fixDate": item["fixDate"],
3980                    "couponStartDate": item["couponStartDate"],
3981                    "couponEndDate": item["couponEndDate"],
3982                }
3983
3984                if calendar is None:
3985                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
3986
3987                else:
3988                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
3989
3990        if calendar is not None:
3991            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
3992
3993            # Saving calendar from Pandas DataFrame to XLSX sheet:
3994            if xlsx:
3995                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
3996
3997                with pd.ExcelWriter(
3998                        path=xlsxCalendarFile,
3999                        date_format=TKS_DATE_FORMAT,
4000                        datetime_format=TKS_DATE_TIME_FORMAT,
4001                        mode="w",
4002                ) as writer:
4003                    humanReadable = calendar.copy(deep=True)
4004                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4005                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4006                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4007                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4008                    humanReadable.columns = colNames  # human-readable column names
4009
4010                    humanReadable.to_excel(
4011                        writer,
4012                        sheet_name="Bond payments calendar",
4013                        index=False,
4014                        encoding="UTF-8",
4015                        freeze_panes=(1, 2),
4016                    )  # saving as XLSX-file with freeze first row and column as headers
4017
4018                    del humanReadable  # release df in memory
4019
4020                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4021
4022        return calendar
4023
4024    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4025        """
4026        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4027        Also, creates Markdown file with calendar data, `calendar.md` by default.
4028
4029        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4030
4031        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4032                        extended information about bonds: main info, current prices, bond payment calendar,
4033                        coupon yields, current yields and some statistics etc.
4034                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4035        :param show: if `True` then also printing bonds payment calendar to the console,
4036                     otherwise save to file `calendarFile` only. `False` by default.
4037        :return: multilines text in Markdown format with bonds payment calendar as a table.
4038        """
4039        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4040            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4041
4042        infoText = "# Bond payments calendar\n\n"
4043
4044        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4045
4046        if not (calendar is None or calendar.empty):
4047            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4048
4049            info = [
4050                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4051                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4052            ]
4053
4054            newMonth = False
4055            notOneBond = calendar["figi"].nunique() > 1
4056            for i, bond in enumerate(calendar.iterrows()):
4057                if newMonth and notOneBond:
4058                    info.append(splitLine)
4059
4060                info.append(
4061                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4062                        "  √" if bond[1]["paid"] else "  —",
4063                        bond[1]["couponDate"].split("T")[0],
4064                        bond[1]["figi"],
4065                        bond[1]["ticker"],
4066                        bond[1]["couponNumber"],
4067                        "{} {}".format(
4068                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4069                            bond[1]["payCurrency"],
4070                        ),
4071                        bond[1]["couponType"],
4072                        bond[1]["couponPeriod"],
4073                        bond[1]["fixDate"].split("T")[0],
4074                    )
4075                )
4076
4077                if i < len(calendar.values) - 1:
4078                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4079                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4080                    newMonth = False if curDate.month == nextDate.month else True
4081
4082                else:
4083                    newMonth = False
4084
4085            infoText += "".join(info)
4086
4087            if show:
4088                uLogger.info("{}".format(infoText))
4089
4090            if self.calendarFile is not None:
4091                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4092                    fH.write(infoText)
4093
4094                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4095
4096        else:
4097            infoText += "No data\n"
4098
4099        return infoText
4100
4101    def OverviewAccounts(self, show: bool = False) -> dict:
4102        """
4103        Method for parsing and show simple table with all available user accounts.
4104
4105        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4106
4107        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4108        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4109                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4110                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4111                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4112                                                        "closed": "—", "access": "Full access" }, ...}}`
4113        """
4114        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4115
4116        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4117        accounts = {
4118            item["id"]: {
4119                "type": TKS_ACCOUNT_TYPES[item["type"]],
4120                "name": item["name"],
4121                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4122                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4123                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4124                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4125            } for item in rawAccounts["accounts"]
4126        }
4127
4128        # Raw and parsed data with some fields replaced in "stat" section:
4129        view = {
4130            "rawAccounts": rawAccounts,
4131            "stat": accounts,
4132        }
4133
4134        # --- Prepare simple text table with only accounts data in human-readable format:
4135        if show:
4136            info = [
4137                "# User accounts\n\n",
4138                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4139                "| Account ID   | Type                      | Status                    | Name                           |\n",
4140                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4141            ]
4142
4143            for account in view["stat"].keys():
4144                info.extend([
4145                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4146                        account,
4147                        view["stat"][account]["type"],
4148                        view["stat"][account]["status"],
4149                        view["stat"][account]["name"],
4150                    )
4151                ])
4152
4153            infoText = "".join(info)
4154
4155            uLogger.info(infoText)
4156
4157            if self.userAccountsFile:
4158                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4159                    fH.write(infoText)
4160
4161                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4162
4163        return view
4164
4165    def OverviewUserInfo(self, show: bool = False) -> dict:
4166        """
4167        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4168
4169        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4170
4171        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4172        :return: dict with raw parsed data from server and some calculated statistics about it.
4173        """
4174        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4175        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4176        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4177        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4178        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4179        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4180
4181        # This is dict with parsed common user data:
4182        userInfo = {
4183            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4184            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4185            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4186            "tariff": rawUserInfo["tariff"],
4187        }
4188
4189        # This is an array of dict with parsed margin statuses for every account IDs:
4190        margins = {}
4191        for accountId in accounts.keys():
4192            if rawMargins[accountId]:
4193                margins[accountId] = {
4194                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4195                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4196                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4197                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4198                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4199                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4200                }
4201
4202            else:
4203                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4204
4205        unary = {}  # unary-connection limits
4206        for item in rawTariffLimits["unaryLimits"]:
4207            if item["limitPerMinute"] in unary.keys():
4208                unary[item["limitPerMinute"]].extend(item["methods"])
4209
4210            else:
4211                unary[item["limitPerMinute"]] = item["methods"]
4212
4213        stream = {}  # stream-connection limits
4214        for item in rawTariffLimits["streamLimits"]:
4215            if item["limit"] in stream.keys():
4216                stream[item["limit"]].extend(item["streams"])
4217
4218            else:
4219                stream[item["limit"]] = item["streams"]
4220
4221        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4222        limits = {
4223            "unary": unary,
4224            "stream": stream,
4225        }
4226
4227        # Raw and parsed data as an output result:
4228        view = {
4229            "rawUserInfo": rawUserInfo,
4230            "rawAccounts": rawAccounts,
4231            "rawMargins": rawMargins,
4232            "rawTariffLimits": rawTariffLimits,
4233            "stat": {
4234                "userInfo": userInfo,
4235                "accounts": accounts,
4236                "margins": margins,
4237                "limits": limits,
4238            },
4239        }
4240
4241        # --- Prepare text table with user information in human-readable format:
4242        if show:
4243            info = [
4244                "# Full user information\n\n",
4245                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4246                "## Common information\n\n",
4247                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4248                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4249                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4250                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4251                "\n## User accounts\n\n",
4252            ]
4253
4254            for account in view["stat"]["accounts"].keys():
4255                info.extend([
4256                    "### ID: [{}]\n\n".format(account),
4257                    "| Parameters           | Values                                                       |\n",
4258                    "|----------------------|--------------------------------------------------------------|\n",
4259                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4260                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4261                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4262                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4263                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4264                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4265                ])
4266
4267                if margins[account]:
4268                    info.extend([
4269                        "| Margin status:       | Enabled                                                      |\n",
4270                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4271                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4272                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4273                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4274                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4275                    ])
4276
4277                else:
4278                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4279
4280            info.extend([
4281                "\n## Current user tariff limits\n",
4282                "\nSee also:\n",
4283                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4284                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4285                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4286                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4287                "\n### Unary limits\n",
4288            ])
4289
4290            if unary:
4291                for key, values in sorted(unary.items()):
4292                    info.append("\n* Max requests per minute: {}\n".format(key))
4293
4294                    for value in values:
4295                        info.append("  - {}\n".format(value))
4296
4297            else:
4298                info.append("\nNot available\n")
4299
4300            info.append("\n### Stream limits\n")
4301
4302            if stream:
4303                for key, values in sorted(stream.items()):
4304                    info.append("\n* Max stream connections: {}\n".format(key))
4305
4306                    for value in values:
4307                        info.append("  - {}\n".format(value))
4308
4309            else:
4310                info.append("\nNot available\n")
4311
4312            infoText = "".join(info)
4313
4314            uLogger.info(infoText)
4315
4316            if self.userInfoFile:
4317                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4318                    fH.write(infoText)
4319
4320                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4321
4322        return view
4323
4324
4325class Args:
4326    """
4327    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4328    """
4329    def __init__(self, **kwargs):
4330        self.__dict__.update(kwargs)
4331
4332    def __getattr__(self, item):
4333        return None
4334
4335
4336def ParseArgs():
4337    """This function get and parse command line keys."""
4338    parser = ArgumentParser()  # command-line string parser
4339
4340    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4341    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4342
4343    # --- options:
4344
4345    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4346    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4347    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4348
4349    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4350    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4351
4352    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4353    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4354
4355    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4356
4357    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4358    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4359    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4360
4361    parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4362    parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.")
4363
4364    # --- commands:
4365
4366    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4367
4368    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4369    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4370    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4371    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4372    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4373    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4374    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4375    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4376
4377    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4378    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4379    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4380    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4381    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4382    parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.")
4383
4384    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4385    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4386    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4387    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4388
4389    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4390    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4391    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4392
4393    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4394    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4395    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4396    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4397    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4398    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4399    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4400
4401    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4402    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4403    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.")
4404    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.")
4405    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.")
4406
4407    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4408    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4409    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4410
4411    cmdArgs = parser.parse_args()
4412    return cmdArgs
4413
4414
4415def Main(**kwargs):
4416    """
4417    Main function for work with TKSBrokerAPI in the console.
4418
4419    See examples:
4420    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4421    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4422    """
4423    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4424
4425    if args.debug_level:
4426        uLogger.level = 10  # always debug level by default
4427        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4428
4429    exitCode = 0
4430    start = datetime.now(tzutc())
4431    uLogger.debug("=-" * 50)
4432    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4433        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4434        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4435    ))
4436
4437    # trying to calculate full current version:
4438    buildVersion = __version__
4439    try:
4440        v = version("tksbrokerapi")
4441        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4442
4443    except Exception:
4444        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4445
4446    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4447    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4448
4449    try:
4450        if args.version:
4451            print("TKSBrokerAPI {}".format(buildVersion))
4452            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4453
4454        else:
4455            # Init class for trading with Tinkoff Broker:
4456            trader = TinkoffBrokerServer(
4457                token=args.token,
4458                accountId=args.account_id,
4459                useCache=not args.no_cache,
4460            )
4461
4462            # --- set some options:
4463
4464            if args.more:
4465                trader.moreDebug = True
4466                uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.")
4467
4468            if args.ticker:
4469                ticker = args.ticker.upper()  # Tickers may be upper case only
4470
4471                if ticker in trader.aliasesKeys:
4472                    trader.ticker = trader.aliases[ticker]  # Replace some tickers with its aliases
4473
4474                else:
4475                    trader.ticker = ticker
4476
4477            if args.figi:
4478                trader.figi = args.figi.upper()  # FIGIs may be upper case only
4479
4480            if args.depth is not None:
4481                trader.depth = args.depth
4482
4483            # --- do one command:
4484
4485            if args.list:
4486                if args.output is not None:
4487                    trader.instrumentsFile = args.output
4488
4489                trader.ShowInstrumentsInfo(show=True)
4490
4491            elif args.list_xlsx:
4492                trader.DumpInstrumentsAsXLSX(forceUpdate=False)
4493
4494            elif args.bonds_xlsx is not None:
4495                if args.output is not None:
4496                    trader.bondsXLSXFile = args.output
4497
4498                if len(args.bonds_xlsx) == 0:
4499                    trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4500
4501                else:
4502                    trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4503
4504            elif args.search:
4505                if args.output is not None:
4506                    trader.searchResultsFile = args.output
4507
4508                trader.SearchInstruments(pattern=args.search[0], show=True)
4509
4510            elif args.info:
4511                if not (args.ticker or args.figi):
4512                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4513                    raise Exception("Ticker or FIGI required")
4514
4515                if args.output is not None:
4516                    trader.infoFile = args.output
4517
4518                if args.ticker:
4519                    trader.SearchByTicker(requestPrice=True, show=True)  # show info and current prices by ticker name
4520
4521                else:
4522                    trader.SearchByFIGI(requestPrice=True, show=True)  # show info and current prices by FIGI id
4523
4524            elif args.calendar is not None:
4525                if args.output is not None:
4526                    trader.calendarFile = args.output
4527
4528                if len(args.calendar) == 0:
4529                    bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4530
4531                else:
4532                    bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4533
4534                trader.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4535
4536            elif args.price:
4537                if not (args.ticker or args.figi):
4538                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4539                    raise Exception("Ticker or FIGI required")
4540
4541                trader.GetCurrentPrices(show=True)
4542
4543            elif args.prices is not None:
4544                if args.output is not None:
4545                    trader.pricesFile = args.output
4546
4547                trader.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4548
4549            elif args.overview:
4550                if args.output is not None:
4551                    trader.overviewFile = args.output
4552
4553                trader.Overview(show=True, details="full")
4554
4555            elif args.overview_digest:
4556                if args.output is not None:
4557                    trader.overviewDigestFile = args.output
4558
4559                trader.Overview(show=True, details="digest")
4560
4561            elif args.overview_positions:
4562                if args.output is not None:
4563                    trader.overviewPositionsFile = args.output
4564
4565                trader.Overview(show=True, details="positions")
4566
4567            elif args.overview_orders:
4568                if args.output is not None:
4569                    trader.overviewOrdersFile = args.output
4570
4571                trader.Overview(show=True, details="orders")
4572
4573            elif args.overview_analytics:
4574                if args.output is not None:
4575                    trader.overviewAnalyticsFile = args.output
4576
4577                trader.Overview(show=True, details="analytics")
4578
4579            elif args.overview_calendar:
4580                if args.output is not None:
4581                    trader.overviewAnalyticsFile = args.output
4582
4583                trader.Overview(show=True, details="calendar")
4584
4585            elif args.deals is not None:
4586                if args.output is not None:
4587                    trader.reportFile = args.output
4588
4589                if 0 <= len(args.deals) < 3:
4590                    trader.Deals(
4591                        start=args.deals[0] if len(args.deals) >= 1 else None,
4592                        end=args.deals[1] if len(args.deals) == 2 else None,
4593                        show=True,  # Always show deals report in console
4594                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
4595                    )
4596
4597                else:
4598                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4599                    raise Exception("Incorrect value")
4600
4601            elif args.history is not None:
4602                if args.output is not None:
4603                    trader.historyFile = args.output
4604
4605                if 0 <= len(args.history) < 3:
4606                    dataReceived = trader.History(
4607                        start=args.history[0] if len(args.history) >= 1 else None,
4608                        end=args.history[1] if len(args.history) == 2 else None,
4609                        interval="hour" if args.interval is None or not args.interval else args.interval,
4610                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
4611                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
4612                        show=True,  # shows all downloaded candles in console
4613                    )
4614
4615                    if args.render_chart is not None and dataReceived is not None:
4616                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4617
4618                        trader.ShowHistoryChart(
4619                            candles=dataReceived,
4620                            interact=iChart,
4621                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4622                        )
4623
4624                else:
4625                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4626                    raise Exception("Incorrect value")
4627
4628            elif args.load_history is not None:
4629                histData = trader.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
4630
4631                if args.render_chart is not None and histData is not None:
4632                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4633                    trader.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
4634
4635                    trader.ShowHistoryChart(
4636                        candles=histData,
4637                        interact=iChart,
4638                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4639                    )
4640
4641            elif args.trade is not None:
4642                if 1 <= len(args.trade) <= 5:
4643                    trader.Trade(
4644                        operation=args.trade[0],
4645                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
4646                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
4647                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
4648                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
4649                    )
4650
4651                else:
4652                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4653
4654            elif args.buy is not None:
4655                if 0 <= len(args.buy) <= 4:
4656                    trader.Buy(
4657                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
4658                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
4659                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
4660                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
4661                    )
4662
4663                else:
4664                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4665
4666            elif args.sell is not None:
4667                if 0 <= len(args.sell) <= 4:
4668                    trader.Sell(
4669                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
4670                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
4671                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
4672                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
4673                    )
4674
4675                else:
4676                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4677
4678            elif args.order:
4679                if 4 <= len(args.order) <= 7:
4680                    trader.Order(
4681                        operation=args.order[0],
4682                        orderType=args.order[1],
4683                        lots=int(args.order[2]),
4684                        targetPrice=float(args.order[3]),
4685                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
4686                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
4687                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
4688                    )
4689
4690                else:
4691                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
4692
4693            elif args.buy_limit:
4694                trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
4695
4696            elif args.sell_limit:
4697                trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
4698
4699            elif args.buy_stop:
4700                if 2 <= len(args.buy_stop) <= 7:
4701                    trader.BuyStop(
4702                        lots=int(args.buy_stop[0]),
4703                        targetPrice=float(args.buy_stop[1]),
4704                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
4705                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
4706                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
4707                    )
4708
4709                else:
4710                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4711
4712            elif args.sell_stop:
4713                if 2 <= len(args.sell_stop) <= 7:
4714                    trader.SellStop(
4715                        lots=int(args.sell_stop[0]),
4716                        targetPrice=float(args.sell_stop[1]),
4717                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
4718                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
4719                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
4720                    )
4721
4722                else:
4723                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
4724
4725            # elif args.buy_order_grid is not None:
4726            #     # update order grid work with api v2
4727            #     if len(args.buy_order_grid) == 2:
4728            #         orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
4729            #
4730            #         for order in orderParams:
4731            #             trader.Order(operation="Buy", lots=order["lot"], price=order["price"])
4732            #
4733            #     else:
4734            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4735            #
4736            # elif args.sell_order_grid is not None:
4737            #     # update order grid work with api v2
4738            #     if len(args.sell_order_grid) >= 2:
4739            #         orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
4740            #
4741            #         for order in orderParams:
4742            #             trader.Order(operation="Sell", lots=order["lot"], price=order["price"])
4743            #
4744            #     else:
4745            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4746
4747            elif args.close_order is not None:
4748                trader.CloseOrders(args.close_order)  # close only one order
4749
4750            elif args.close_orders is not None:
4751                trader.CloseOrders(args.close_orders)  # close list of orders
4752
4753            elif args.close_trade:
4754                if not (args.ticker or args.figi):
4755                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4756                    raise Exception("Ticker or FIGI required")
4757
4758                if args.ticker:
4759                    trader.CloseTrades([args.ticker])  # close only one trade by ticker (priority)
4760
4761                else:
4762                    trader.CloseTrades([args.figi])  # close only one trade by FIGI
4763
4764            elif args.close_trades is not None:
4765                trader.CloseTrades(args.close_trades)  # close trades for list of tickers
4766
4767            elif args.close_all is not None:
4768                trader.CloseAll(*args.close_all)
4769
4770            elif args.limits:
4771                if args.output is not None:
4772                    trader.withdrawalLimitsFile = args.output
4773
4774                trader.OverviewLimits(show=True)
4775
4776            elif args.user_info:
4777                if args.output is not None:
4778                    trader.userInfoFile = args.output
4779
4780                trader.OverviewUserInfo(show=True)
4781
4782            elif args.account:
4783                if args.output is not None:
4784                    trader.userAccountsFile = args.output
4785
4786                trader.OverviewAccounts(show=True)
4787
4788            else:
4789                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
4790                raise Exception("There is no command to execute")
4791
4792    except Exception:
4793        trace = tb.format_exc()
4794        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
4795            if e in trace:
4796                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
4797                break
4798
4799        uLogger.debug(trace)
4800        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
4801        exitCode = 255  # an error occurred, must be open a ticket for this issue
4802
4803    finally:
4804        finish = datetime.now(tzutc())
4805
4806        if exitCode == 0:
4807            if args.more:
4808                uLogger.debug("All operations were finished success (summary code is 0).")
4809
4810        else:
4811            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
4812                os.path.abspath(uLog.defaultLogFile), exitCode,
4813            ))
4814
4815        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
4816        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
4817            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4818            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4819        ))
4820        uLogger.debug("=-" * 50)
4821
4822        if not kwargs:
4823            sys.exit(exitCode)
4824
4825        else:
4826            return exitCode
4827
4828
4829if __name__ == "__main__":
4830    Main()
class TinkoffBrokerServer:
  76class TinkoffBrokerServer:
  77    """
  78    This class implements methods to work with Tinkoff broker server.
  79
  80    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
  81
  82    About `token`: https://tinkoff.github.io/investAPI/token/
  83    """
  84    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
  85        """
  86        Main class init.
  87
  88        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
  89        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
  90                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
  91        :param useCache: use default cache file with raw data to use instead of `iList`.
  92                         True by default. Cache is auto-update if new day has come.
  93                         If you don't want to use cache and always updates raw data then set `useCache=False`.
  94        :param defaultCache: path to default cache file. `dump.json` by default.
  95        """
  96        if token is None or not token:
  97            try:
  98                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
  99                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 100
 101            except KeyError:
 102                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 103                raise Exception("Token required")
 104
 105        else:
 106            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 107            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 108
 109        if accountId is None or not accountId:
 110            try:
 111                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 112                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 113
 114            except KeyError:
 115                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 116
 117        else:
 118            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 119            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 120
 121        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 122        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 123
 124        Latest version: https://pypi.org/project/tksbrokerapi/
 125        """
 126
 127        self.aliases = TKS_TICKER_ALIASES
 128        """Some aliases instead official tickers.
 129
 130        See also: `TKSEnums.TKS_TICKER_ALIASES`
 131        """
 132
 133        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 134
 135        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 136
 137        self.ticker = ""
 138        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 139
 140        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 141        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 142
 143        See also: `SearchByTicker()`, `SearchInstruments()`.
 144        """
 145
 146        self.figi = ""
 147        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 148
 149        See also: `SearchByFIGI()`, `SearchInstruments()`.
 150        """
 151
 152        self.depth = 1
 153        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 154
 155        See also: `GetCurrentPrices()`.
 156        """
 157
 158        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 159        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 160
 161        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 162        """
 163
 164        uLogger.debug("Broker API server: {}".format(self.server))
 165
 166        self.timeout = 15
 167        """Server operations timeout in seconds. Default: `15`.
 168
 169        See also: `SendAPIRequest()`.
 170        """
 171
 172        self.headers = {
 173            "Content-Type": "application/json",
 174            "accept": "application/json",
 175            "Authorization": "Bearer {}".format(self.token),
 176            "x-app-name": "Tim55667757.TKSBrokerAPI",
 177        }
 178        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 179
 180        See also: `SendAPIRequest()`.
 181        """
 182
 183        self.body = None
 184        """Request body which send to broker server. Default: `None`.
 185
 186        See also: `SendAPIRequest()`.
 187        """
 188
 189        self.moreDebug = False
 190        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
 191
 192        self.historyFile = None
 193        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 194
 195        See also: `History()`.
 196        """
 197
 198        self.htmlHistoryFile = "index.html"
 199        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 200
 201        See also: `ShowHistoryChart()`.
 202        """
 203
 204        self.instrumentsFile = "instruments.md"
 205        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 206
 207        See also: `ShowInstrumentsInfo()`.
 208        """
 209
 210        self.searchResultsFile = "search-results.md"
 211        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 212
 213        See also: `SearchInstruments()`.
 214        """
 215
 216        self.pricesFile = "prices.md"
 217        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 218
 219        See also: `GetListOfPrices()`.
 220        """
 221
 222        self.infoFile = "info.md"
 223        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 224
 225        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 226        """
 227
 228        self.bondsXLSXFile = "ext-bonds.xlsx"
 229        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 230        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 231
 232        See also: `ExtendBondsData()`.
 233        """
 234
 235        self.calendarFile = "calendar.md"
 236        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 237        
 238        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 239
 240        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 241        """
 242
 243        self.overviewFile = "overview.md"
 244        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 245
 246        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 247        """
 248
 249        self.overviewDigestFile = "overview-digest.md"
 250        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 251
 252        See also: `Overview()` with parameter `details="digest"`.
 253        """
 254
 255        self.overviewPositionsFile = "overview-positions.md"
 256        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 257
 258        See also: `Overview()` with parameter `details="positions"`.
 259        """
 260
 261        self.overviewOrdersFile = "overview-orders.md"
 262        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 263
 264        See also: `Overview()` with parameter `details="orders"`.
 265        """
 266
 267        self.overviewAnalyticsFile = "overview-analytics.md"
 268        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 269
 270        See also: `Overview()` with parameter `details="analytics"`.
 271        """
 272
 273        self.overviewBondsCalendarFile = "overview-calendar.md"
 274        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
 275
 276        See also: `Overview()` with parameter `details="calendar"`.
 277        """
 278
 279        self.reportFile = "deals.md"
 280        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 281
 282        See also: `Deals()`.
 283        """
 284
 285        self.withdrawalLimitsFile = "limits.md"
 286        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 287
 288        See also: `OverviewLimits()` and `RequestLimits()`.
 289        """
 290
 291        self.userInfoFile = "user-info.md"
 292        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 293
 294        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 295        """
 296
 297        self.userAccountsFile = "accounts.md"
 298        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 299
 300        See also: `OverviewAccounts()`, `RequestAccounts()`.
 301        """
 302
 303        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 304        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 305
 306        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 307
 308        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 309        """
 310
 311        self.iList = None  # init iList for raw instruments data
 312        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 313        
 314        See also: `Listing()`, `DumpInstruments()`.
 315        """
 316
 317        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 318        if useCache:
 319            if os.path.exists(self.iListDumpFile):
 320                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 321                curTime = datetime.now(tzutc())
 322
 323                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 324                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 325
 326                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 327
 328                else:
 329                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 330
 331                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
 332                        os.path.abspath(self.iListDumpFile),
 333                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
 334                    ))
 335
 336            else:
 337                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 338                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 339
 340        else:
 341            self.iList = self.Listing()  # request new raw instruments data from broker server
 342            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 343
 344        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 345        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 346
 347        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 348        """
 349
 350    def _ParseJSON(self, rawData="{}") -> dict:
 351        """
 352        Parse JSON from response string.
 353
 354        :param rawData: this is a string with JSON-formatted text.
 355        :return: JSON (dictionary), parsed from server response string.
 356        """
 357        responseJSON = json.loads(rawData) if rawData else {}
 358
 359        if self.moreDebug:
 360            uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
 361
 362        return responseJSON
 363
 364    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
 365        """
 366        Send GET or POST request to broker server and receive JSON object.
 367
 368        self.header: must be defining with dictionary of headers.
 369        self.body: if define then used as request body. None by default.
 370        self.timeout: global request timeout, 15 seconds by default.
 371        :param url: url with REST request.
 372        :param reqType: send "GET" or "POST" request. "GET" by default.
 373        :param retry: how many times retry after first request if an 5xx server errors occurred.
 374        :param pause: sleep time in seconds between retries.
 375        :return: response JSON (dictionary) from broker.
 376        """
 377        if reqType not in ("GET", "POST"):
 378            uLogger.error("You can define request type: 'GET' or 'POST'!")
 379            raise Exception("Incorrect value")
 380
 381        if self.moreDebug:
 382            uLogger.debug("Request parameters:")
 383            uLogger.debug("    - REST API URL: {}".format(url))
 384            uLogger.debug("    - request type: {}".format(reqType))
 385            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
 386            uLogger.debug("    - body:\n{}".format(self.body))
 387
 388        # fast hack to avoid all operations with some tickers/FIGI
 389        responseJSON = {}
 390        oK = True
 391        for item in self.exclude:
 392            if item in url:
 393                if self.moreDebug:
 394                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 395
 396                oK = False
 397                break
 398
 399        if oK:
 400            counter = 0
 401            response = None
 402            errMsg = ""
 403
 404            while not response and counter <= retry:
 405                if reqType == "GET":
 406                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 407
 408                if reqType == "POST":
 409                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 410
 411                if self.moreDebug:
 412                    uLogger.debug("Response:")
 413                    uLogger.debug("    - status code: {}".format(response.status_code))
 414                    uLogger.debug("    - reason: {}".format(response.reason))
 415                    uLogger.debug("    - body length: {}".format(len(response.text)))
 416                    uLogger.debug("    - headers:\n{}".format(response.headers))
 417
 418                # Server returns some headers:
 419                # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
 420                # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
 421                # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
 422                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 423                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 424                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
 425                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 426                    sleep(rateLimitWait)
 427
 428                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 429                if 400 <= response.status_code < 500:
 430                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 431                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 432                    counter = retry + 1
 433
 434                if 500 <= response.status_code < 600:
 435                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 436                    uLogger.debug("    - not oK, {}".format(errMsg))
 437                    counter += 1
 438
 439                    if counter <= retry:
 440                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 441                        sleep(pause)
 442
 443            responseJSON = self._ParseJSON(rawData=response.text)
 444
 445            if errMsg:
 446                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 447                uLogger.error("    - not oK, {}".format(errMsg))
 448
 449        return responseJSON
 450
 451    def _IUpdater(self, iType: str) -> tuple:
 452        """
 453        Request instrument by type from server. See available API methods for instruments:
 454        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 455        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 456        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 457        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 458        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 459
 460        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 461        :return: tuple with iType name and list of available instruments of current type for defined user token.
 462        """
 463        result = []
 464
 465        if iType in TKS_INSTRUMENTS:
 466            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 467
 468            # all instruments have the same body in API v2 requests:
 469            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 470            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 471            result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
 472
 473        return iType, result
 474
 475    def _IWrapper(self, kwargs):
 476        """
 477        Wrapper runs instrument's update method `_IUpdater()`.
 478        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 479        """
 480        return self._IUpdater(**kwargs)
 481
 482    def Listing(self) -> dict:
 483        """
 484        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 485
 486        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 487        """
 488        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 489        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 490
 491        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 492        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 493        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 494
 495        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 496        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 497        poolUpdater.close()
 498
 499        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 500        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 501        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 502
 503        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 504        for iType in iList.keys():
 505            for ticker in iList[iType]:
 506                iList[iType][ticker]["type"] = iType
 507
 508                if "minPriceIncrement" in iList[iType][ticker].keys():
 509                    iList[iType][ticker]["step"] = NanoToFloat(
 510                        iList[iType][ticker]["minPriceIncrement"]["units"],
 511                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 512                    )
 513
 514                else:
 515                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 516
 517        return iList
 518
 519    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 520        """
 521        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 522
 523        See also: `DumpInstruments()`, `Listing()`.
 524
 525        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 526                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 527        """
 528        if self.iListDumpFile is None or not self.iListDumpFile:
 529            uLogger.error("Output name of dump file must be defined!")
 530            raise Exception("Filename required")
 531
 532        if not self.iList or forceUpdate:
 533            self.iList = self.Listing()
 534
 535        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 536
 537        # Save as XLSX with separated sheets for every type of instruments:
 538        with pd.ExcelWriter(
 539                path=xlsxDumpFile,
 540                date_format=TKS_DATE_FORMAT,
 541                datetime_format=TKS_DATE_TIME_FORMAT,
 542                mode="w",
 543        ) as writer:
 544            for iType in TKS_INSTRUMENTS:
 545                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 546                df = df[sorted(df)]  # sorted by column names
 547                df = df.applymap(
 548                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 549                    na_action="ignore",
 550                )  # converting numbers from nano-type to float in every cell
 551                df.to_excel(
 552                    writer,
 553                    sheet_name=iType,
 554                    encoding="UTF-8",
 555                    freeze_panes=(1, 1),
 556                )  # saving as XLSX-file with freeze first row and column as headers
 557
 558        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 559
 560    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 561        """
 562        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 563        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 564
 565        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 566
 567        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 568                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 569        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 570        """
 571        if self.iListDumpFile is None or not self.iListDumpFile:
 572            uLogger.error("Output name of dump file must be defined!")
 573            raise Exception("Filename required")
 574
 575        if not self.iList or forceUpdate:
 576            self.iList = self.Listing()
 577
 578        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 579        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 580            fH.write(jsonDump)
 581
 582        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 583
 584        return jsonDump
 585
 586    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
 587        """
 588        Show information about one instrument defined by json data and prints it in Markdown format.
 589
 590        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 591
 592        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]`
 593        :param show: if `True` then also printing information about instrument and its current price.
 594        :return: multilines text in Markdown format with information about one instrument.
 595        """
 596        splitLine = "|                                                             |                                                        |\n"
 597        infoText = ""
 598
 599        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 600            info = [
 601                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
 602                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
 603                "| Parameters                                                  | Values                                                 |\n",
 604                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 605                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 606                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 607            ]
 608
 609            if "sector" in iJSON.keys() and iJSON["sector"]:
 610                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 611
 612            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
 613                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
 614
 615            info.extend([
 616                splitLine,
 617                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 618                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
 619            ])
 620
 621            if "isin" in iJSON.keys() and iJSON["isin"]:
 622                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 623
 624            if "classCode" in iJSON.keys():
 625                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 626
 627            info.extend([
 628                splitLine,
 629                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 630                splitLine,
 631                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 632                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 633                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 634            ])
 635
 636            if iJSON["figi"]:
 637                self.figi = iJSON["figi"]
 638                iJSON = iJSON | self.RequestTradingStatus()
 639
 640                info.extend([
 641                    splitLine,
 642                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 643                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 644                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 645                ])
 646
 647            info.append(splitLine)
 648
 649            if "type" in iJSON.keys() and iJSON["type"]:
 650                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 651
 652                if "shareType" in iJSON.keys() and iJSON["shareType"]:
 653                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
 654
 655            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 656                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 657
 658            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 659                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 660
 661            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 662                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 663
 664            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 665                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 666
 667            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 668                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 669
 670            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 671                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 672
 673            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 674                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 675
 676            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 677                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 678
 679            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 680                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 681
 682            if "currency" in iJSON.keys():
 683                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 684
 685            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 686                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 687
 688            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 689                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 690
 691            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 692                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 693
 694            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 695                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 696
 697            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 698                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 699
 700            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 701                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 702
 703            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 704                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 705
 706            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 707                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 708
 709            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 710                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 711
 712            iExt = None
 713            if iJSON["type"] == "Bonds":
 714                info.extend([
 715                    splitLine,
 716                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 717                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 718                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 719                        iJSON["nominal"]["currency"],
 720                    )),
 721                ])
 722
 723                if "floatingCouponFlag" in iJSON.keys():
 724                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 725
 726                if "amortizationFlag" in iJSON.keys():
 727                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 728
 729                info.append(splitLine)
 730
 731                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 732                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 733
 734                if iJSON["figi"]:
 735                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 736
 737                    info.extend([
 738                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 739                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 740                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 741                    ])
 742
 743                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 744                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 745                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 746                        iJSON["aciValue"]["currency"]
 747                    )))
 748
 749            if "currentPrice" in iJSON.keys():
 750                info.append(splitLine)
 751
 752                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 753                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 754
 755                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 756                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 757                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 758                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 759                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 760
 761                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 762                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 763
 764                info.extend([
 765                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 766                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 767                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 768                    )),
 769                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 770                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 771                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 772                    )),
 773                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 774                        "{:.2f}%{}".format(
 775                            iJSON["currentPrice"]["changes"],
 776                            " ({}{:.2f} {})".format(
 777                                "+" if bondChangesDelta > 0 else "",
 778                                bondChangesDelta,
 779                                aciCurrency
 780                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 781                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 782                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 783                                currency
 784                            ),
 785                        )
 786                    ),
 787                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 788                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 789                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 790                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 791                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 792                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 793                    )),
 794                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 795                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 796                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 797                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 798                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 799                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 800                    )),
 801                ])
 802
 803            if "lot" in iJSON.keys():
 804                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 805
 806            if "step" in iJSON.keys() and iJSON["step"] != 0:
 807                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
 808
 809            # Add bond payment calendar:
 810            if iJSON["type"] == "Bonds":
 811                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 812                info.extend(["\n", strCalendar])
 813
 814            infoText += "".join(info)
 815
 816            if show:
 817                uLogger.info("{}".format(infoText))
 818
 819            else:
 820                uLogger.debug("{}".format(infoText))
 821
 822            if self.infoFile is not None:
 823                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 824                    fH.write(infoText)
 825
 826                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 827
 828        return infoText
 829
 830    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 831        """
 832        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 833
 834        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 835        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 836        :return: JSON formatted data with information about instrument.
 837        """
 838        tickerJSON = {}
 839        if self.moreDebug:
 840            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker))
 841
 842        if not self.ticker:
 843            uLogger.warning("self.ticker variable is not be empty!")
 844
 845        else:
 846            if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 847                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker))
 848                raise Exception("Instrument not allowed")
 849
 850            if not self.iList:
 851                self.iList = self.Listing()
 852
 853            if self.ticker in self.iList["Shares"].keys():
 854                tickerJSON = self.iList["Shares"][self.ticker]
 855                if self.moreDebug:
 856                    uLogger.debug("Ticker [{}] found in shares list".format(self.ticker))
 857
 858            elif self.ticker in self.iList["Currencies"].keys():
 859                tickerJSON = self.iList["Currencies"][self.ticker]
 860                if self.moreDebug:
 861                    uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker))
 862
 863            elif self.ticker in self.iList["Bonds"].keys():
 864                tickerJSON = self.iList["Bonds"][self.ticker]
 865                if self.moreDebug:
 866                    uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker))
 867
 868            elif self.ticker in self.iList["Etfs"].keys():
 869                tickerJSON = self.iList["Etfs"][self.ticker]
 870                if self.moreDebug:
 871                    uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker))
 872
 873            elif self.ticker in self.iList["Futures"].keys():
 874                tickerJSON = self.iList["Futures"][self.ticker]
 875                if self.moreDebug:
 876                    uLogger.debug("Ticker [{}] found in futures list".format(self.ticker))
 877
 878        if tickerJSON:
 879            self.figi = tickerJSON["figi"]
 880
 881            if requestPrice:
 882                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 883
 884                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 885                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 886
 887                else:
 888                    tickerJSON["currentPrice"]["changes"] = 0
 889
 890            if show:
 891                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 892
 893        else:
 894            if show:
 895                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker))
 896
 897        return tickerJSON
 898
 899    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 900        """
 901        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
 902
 903        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
 904        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 905        :return: JSON formatted data with information about instrument.
 906        """
 907        figiJSON = {}
 908        if self.moreDebug:
 909            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi))
 910
 911        if not self.figi:
 912            uLogger.warning("self.figi variable is not be empty!")
 913
 914        else:
 915            if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
 916                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi))
 917                raise Exception("Instrument not allowed")
 918
 919            if not self.iList:
 920                self.iList = self.Listing()
 921
 922            for item in self.iList["Shares"].keys():
 923                if self.figi == self.iList["Shares"][item]["figi"]:
 924                    figiJSON = self.iList["Shares"][item]
 925
 926                    if self.moreDebug:
 927                        uLogger.debug("FIGI [{}] found in shares list".format(self.figi))
 928
 929                    break
 930
 931            if not figiJSON:
 932                for item in self.iList["Currencies"].keys():
 933                    if self.figi == self.iList["Currencies"][item]["figi"]:
 934                        figiJSON = self.iList["Currencies"][item]
 935
 936                        if self.moreDebug:
 937                            uLogger.debug("FIGI [{}] found in currencies list".format(self.figi))
 938
 939                        break
 940
 941            if not figiJSON:
 942                for item in self.iList["Bonds"].keys():
 943                    if self.figi == self.iList["Bonds"][item]["figi"]:
 944                        figiJSON = self.iList["Bonds"][item]
 945
 946                        if self.moreDebug:
 947                            uLogger.debug("FIGI [{}] found in bonds list".format(self.figi))
 948
 949                        break
 950
 951            if not figiJSON:
 952                for item in self.iList["Etfs"].keys():
 953                    if self.figi == self.iList["Etfs"][item]["figi"]:
 954                        figiJSON = self.iList["Etfs"][item]
 955
 956                        if self.moreDebug:
 957                            uLogger.debug("FIGI [{}] found in etfs list".format(self.figi))
 958
 959                        break
 960
 961            if not figiJSON:
 962                for item in self.iList["Futures"].keys():
 963                    if self.figi == self.iList["Futures"][item]["figi"]:
 964                        figiJSON = self.iList["Futures"][item]
 965
 966                        if self.moreDebug:
 967                            uLogger.debug("FIGI [{}] found in futures list".format(self.figi))
 968
 969                        break
 970
 971        if figiJSON:
 972            self.figi = figiJSON["figi"]
 973            self.ticker = figiJSON["ticker"]
 974
 975            if requestPrice:
 976                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 977
 978                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
 979                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
 980
 981                else:
 982                    figiJSON["currentPrice"]["changes"] = 0
 983
 984            if show:
 985                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
 986
 987        else:
 988            if show:
 989                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi))
 990
 991        return figiJSON
 992
 993    def GetCurrentPrices(self, show: bool = True) -> dict:
 994        """
 995        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
 996        `{"buy": [{"price": 1243.8, "quantity": 193},
 997                  {"price": 1244.0, "quantity": 168},
 998                  {"price": 1244.8, "quantity": 5},
 999                  {"price": 1245.0, "quantity": 61},
1000                  {"price": 1245.4, "quantity": 60}],
1001          "sell": [{"price": 1243.6, "quantity": 8},
1002                   {"price": 1242.6, "quantity": 10},
1003                   {"price": 1242.4, "quantity": 18},
1004                   {"price": 1242.2, "quantity": 50},
1005                   {"price": 1242.0, "quantity": 113}],
1006          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1007        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1008        - sell: list of dicts with Buyers prices,
1009            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1010            - quantity: volume value by current price in lots,
1011        - limitUp: current trade session limit price, maximum,
1012        - limitDown: current trade session limit price, minimum,
1013        - lastPrice: last deal price of the instrument,
1014        - closePrice: previous trade session close price of the instrument.
1015
1016        See also: `SearchByTicker()` and `SearchByFIGI()`.
1017        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1018        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1019
1020        :param show: if `True` then print DOM to log and console.
1021        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1022                 If an error occurred then returns an empty record:
1023                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1024        """
1025        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1026
1027        if self.depth < 1:
1028            uLogger.error("Depth of Market (DOM) must be >=1!")
1029            raise Exception("Incorrect value")
1030
1031        if not (self.ticker or self.figi):
1032            uLogger.error("self.ticker or self.figi variables must be defined!")
1033            raise Exception("Ticker or FIGI required")
1034
1035        if self.ticker and not self.figi:
1036            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1037            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1038
1039        if not self.ticker and self.figi:
1040            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1041            self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1042
1043        if not self.figi:
1044            uLogger.error("FIGI is not defined!")
1045            raise Exception("Ticker or FIGI required")
1046
1047        else:
1048            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi))
1049
1050            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1051            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1052            self.body = str({"figi": self.figi, "depth": self.depth})
1053            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1054
1055            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1056                # list of dicts with sellers orders:
1057                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1058
1059                # list of dicts with buyers orders:
1060                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1061
1062                # max price of instrument at this time:
1063                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1064
1065                # min price of instrument at this time:
1066                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1067
1068                # last price of deal with instrument:
1069                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1070
1071                # last close price of instrument:
1072                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1073
1074            else:
1075                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1076                uLogger.debug("Server response: {}".format(pricesResponse))
1077
1078            if show:
1079                if prices["buy"] or prices["sell"]:
1080                    info = [
1081                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1082                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1083                            self.ticker,
1084                            self.figi,
1085                            self.depth,
1086                        ),
1087                        "-" * 60, "\n",
1088                        "             Orders of Buyers | Orders of Sellers\n",
1089                        "-" * 60, "\n",
1090                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1091                        "-" * 60, "\n",
1092                    ]
1093
1094                    if not prices["buy"]:
1095                        info.append("                              | No orders!\n")
1096                        sumBuy = 0
1097
1098                    else:
1099                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1100                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1101                        for item in maxMinSorted:
1102                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1103
1104                    if not prices["sell"]:
1105                        info.append("No orders!                    |\n")
1106                        sumSell = 0
1107
1108                    else:
1109                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1110                        for item in prices["sell"]:
1111                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1112
1113                    info.extend([
1114                        "-" * 60, "\n",
1115                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1116                        "-" * 60, "\n",
1117                    ])
1118
1119                    infoText = "".join(info)
1120
1121                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1122
1123                else:
1124                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1125
1126        return prices
1127
1128    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1129        """
1130        This method get and show information about all available broker instruments for current user account.
1131        If `instrumentsFile` string is not empty then also save information to this file.
1132
1133        :param show: if `True` then print results to console, if `False` — print only to file.
1134        :return: multi-lines string with all available broker instruments
1135        """
1136        if not self.iList:
1137            self.iList = self.Listing()
1138
1139        info = [
1140            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1141            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1142        ]
1143
1144        # add instruments count by type:
1145        for iType in self.iList.keys():
1146            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1147
1148        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1149        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1150
1151        # generating info tables with all instruments by type:
1152        for iType in self.iList.keys():
1153            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1154
1155            for instrument in self.iList[iType].keys():
1156                iName = self.iList[iType][instrument]["name"]  # instrument's name
1157                if len(iName) > 57:
1158                    iName = "{}...".format(iName[:54])  # right trim for a long string
1159
1160                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1161                    self.iList[iType][instrument]["ticker"],
1162                    iName,
1163                    self.iList[iType][instrument]["figi"],
1164                    self.iList[iType][instrument]["currency"],
1165                    self.iList[iType][instrument]["lot"],
1166                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1167                ))
1168
1169        infoText = "".join(info)
1170
1171        if show:
1172            uLogger.info(infoText)
1173
1174        if self.instrumentsFile:
1175            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1176                fH.write(infoText)
1177
1178            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1179
1180        return infoText
1181
1182    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1183        """
1184        This method search and show information about instruments by part of its ticker, FIGI or name.
1185        If `searchResultsFile` string is not empty then also save information to this file.
1186
1187        :param pattern: string with part of ticker, FIGI or instrument's name.
1188        :param show: if `True` then print results to console, if `False` — return list of result only.
1189        :return: list of dictionaries with all found instruments.
1190        """
1191        if not self.iList:
1192            self.iList = self.Listing()
1193
1194        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1195        compiledPattern = re.compile(pattern, re.IGNORECASE)
1196
1197        for iType in self.iList:
1198            for instrument in self.iList[iType].values():
1199                searchResult = compiledPattern.search(" ".join(
1200                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1201                ))
1202
1203                if searchResult:
1204                    searchResults[iType][instrument["ticker"]] = instrument
1205
1206        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1207        info = [
1208            "# Search results\n\n",
1209            "* **Search pattern:** [{}]\n".format(pattern),
1210            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1211            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1212        ]
1213        infoShort = info[:]
1214
1215        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1216        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1217        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1218
1219        if resultsLen == 0:
1220            info.append("\nNo results\n")
1221            infoShort.append("\nNo results\n")
1222            uLogger.warning("No results. Try changing your search pattern.")
1223
1224        else:
1225            for iType in searchResults:
1226                iTypeValuesCount = len(searchResults[iType].values())
1227                if iTypeValuesCount > 0:
1228                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1229                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1230
1231                    for instrument in searchResults[iType].values():
1232                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1233                            instrument["type"],
1234                            instrument["ticker"],
1235                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1236                            instrument["figi"],
1237                        ))
1238
1239                    if iTypeValuesCount <= 5:
1240                        infoShort.extend(info[-iTypeValuesCount:])
1241
1242                    else:
1243                        infoShort.extend(info[-5:])
1244                        infoShort.append(skippedLine)
1245
1246        infoText = "".join(info)
1247        infoTextShort = "".join(infoShort)
1248
1249        if show:
1250            uLogger.info(infoTextShort)
1251            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1252
1253        if self.searchResultsFile:
1254            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1255                fH.write(infoText)
1256
1257            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1258
1259        return searchResults
1260
1261    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1262        """
1263        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1264
1265        :param instruments: list of strings with tickers or FIGIs.
1266        :return: list with unique instrument FIGIs only.
1267        """
1268        requestedInstruments = []
1269        for iName in instruments:
1270            if iName not in self.aliases.keys():
1271                if iName not in requestedInstruments:
1272                    requestedInstruments.append(iName)
1273
1274            else:
1275                if iName not in requestedInstruments:
1276                    if self.aliases[iName] not in requestedInstruments:
1277                        requestedInstruments.append(self.aliases[iName])
1278
1279        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1280
1281        onlyUniqueFIGIs = []
1282        for iName in requestedInstruments:
1283            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1284                continue
1285
1286            self.ticker = iName
1287            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1288
1289            if not iData:
1290                self.ticker = ""
1291                self.figi = iName
1292
1293                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1294
1295                if not iData:
1296                    self.figi = ""
1297                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1298
1299            if iData and iData["figi"] not in onlyUniqueFIGIs:
1300                onlyUniqueFIGIs.append(iData["figi"])
1301
1302        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1303
1304        return onlyUniqueFIGIs
1305
1306    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1307        """
1308        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1309
1310        See limits: https://tinkoff.github.io/investAPI/limits/
1311
1312        If `pricesFile` string is not empty then also save information to this file.
1313
1314        :param instruments: list of strings with tickers or FIGIs.
1315        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1316        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1317                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1318        """
1319        if instruments is None or not instruments:
1320            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1321            raise Exception("Ticker or FIGI required")
1322
1323        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1324
1325        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1326
1327        iList = []  # trying to get info and current prices about all unique instruments:
1328        for self.figi in onlyUniqueFIGIs:
1329            iData = self.SearchByFIGI(requestPrice=True)
1330            iList.append(iData)
1331
1332        self.ShowListOfPrices(iList, show)
1333
1334        return iList
1335
1336    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1337        """
1338        Show table contains current prices of given instruments.
1339
1340        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1341                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1342        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1343        :return: multilines text in Markdown format as a table contains current prices.
1344        """
1345        infoText = ""
1346
1347        if show or self.pricesFile:
1348            info = [
1349                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1350                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1351                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1352            ]
1353
1354            for item in iList:
1355                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1356                    item["ticker"],
1357                    item["figi"],
1358                    item["type"],
1359                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1360                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1361                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1362                    "{} / {}".format(
1363                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1364                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1365                    ),
1366                    "{} / {}".format(
1367                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1368                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1369                    ),
1370                    item["currency"],
1371                ))
1372
1373            infoText = "".join(info)
1374
1375            if show:
1376                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1377
1378            if self.pricesFile:
1379                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1380                    fH.write(infoText)
1381
1382                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1383
1384        return infoText
1385
1386    def RequestTradingStatus(self) -> dict:
1387        """
1388        Requesting trading status for the instrument defined by `figi` variable.
1389
1390        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1391
1392        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1393
1394        :return: dictionary with trading status attributes. Response example:
1395                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1396                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1397        """
1398        if self.figi is None or not self.figi:
1399            uLogger.error("Variable `figi` must be defined for using this method!")
1400            raise Exception("FIGI required")
1401
1402        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi))
1403
1404        self.body = str({"figi": self.figi, "instrumentId": self.figi})
1405        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1406        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1407
1408        if self.moreDebug:
1409            uLogger.debug("Records about current trading status successfully received")
1410
1411        return tradingStatus
1412
1413    def RequestPortfolio(self) -> dict:
1414        """
1415        Requesting actual user's portfolio for current `accountId`.
1416
1417        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1418
1419        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1420
1421        :return: dictionary with user's portfolio.
1422        """
1423        if self.accountId is None or not self.accountId:
1424            uLogger.error("Variable `accountId` must be defined for using this method!")
1425            raise Exception("Account ID required")
1426
1427        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1428
1429        self.body = str({"accountId": self.accountId})
1430        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1431        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1432
1433        if self.moreDebug:
1434            uLogger.debug("Records about user's portfolio successfully received")
1435
1436        return rawPortfolio
1437
1438    def RequestPositions(self) -> dict:
1439        """
1440        Requesting open positions by currencies and instruments for current `accountId`.
1441
1442        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1443
1444        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1445
1446        :return: dictionary with open positions by instruments.
1447        """
1448        if self.accountId is None or not self.accountId:
1449            uLogger.error("Variable `accountId` must be defined for using this method!")
1450            raise Exception("Account ID required")
1451
1452        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1453
1454        self.body = str({"accountId": self.accountId})
1455        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1456        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1457
1458        if self.moreDebug:
1459            uLogger.debug("Records about current open positions successfully received")
1460
1461        return rawPositions
1462
1463    def RequestPendingOrders(self) -> list:
1464        """
1465        Requesting current actual pending orders for current `accountId`.
1466
1467        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1468
1469        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1470
1471        :return: list of dictionaries with pending orders.
1472        """
1473        if self.accountId is None or not self.accountId:
1474            uLogger.error("Variable `accountId` must be defined for using this method!")
1475            raise Exception("Account ID required")
1476
1477        uLogger.debug("Requesting current actual pending orders. Wait, please...")
1478
1479        self.body = str({"accountId": self.accountId})
1480        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1481        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1482
1483        uLogger.debug("[{}] records about pending orders received".format(len(rawOrders)))
1484
1485        return rawOrders
1486
1487    def RequestStopOrders(self) -> list:
1488        """
1489        Requesting current actual stop orders for current `accountId`.
1490
1491        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1492
1493        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1494
1495        :return: list of dictionaries with stop orders.
1496        """
1497        if self.accountId is None or not self.accountId:
1498            uLogger.error("Variable `accountId` must be defined for using this method!")
1499            raise Exception("Account ID required")
1500
1501        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1502
1503        self.body = str({"accountId": self.accountId})
1504        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1505        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1506
1507        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1508
1509        return rawStopOrders
1510
1511    def Overview(self, show: bool = False, details: str = "full") -> dict:
1512        """
1513        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1514        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1515        and `overviewBondsCalendarFile` are defined then also save information to file.
1516
1517        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1518        many requests about the state of the portfolio, and then, based on the received data, a large number
1519        of calculation and statistics are collected.
1520
1521        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1522        :param details: how detailed should the information be?
1523        - `full` — shows full available information about portfolio status (by default),
1524        - `positions` — shows only open positions,
1525        - `orders` — shows only sections of open limits and stop orders.
1526        - `digest` — show a short digest of the portfolio status,
1527        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1528        - `calendar` — shows only the bonds calendar section (if these present in portfolio),
1529        :return: dictionary with client's raw portfolio and some statistics.
1530        """
1531        if self.accountId is None or not self.accountId:
1532            uLogger.error("Variable `accountId` must be defined for using this method!")
1533            raise Exception("Account ID required")
1534
1535        view = {
1536            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1537                "headers": {},  # list of dictionaries, response headers without "positions" section
1538                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1539                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1540                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1541                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1542                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1543                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1544                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1545                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1546                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1547            },
1548            "stat": {  # --- some statistics calculated using "raw" sections:
1549                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1550                "availableRUB": 0.,  # available rubles (without other currencies)
1551                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1552                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1553                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1554                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1555                "sharesCostRUB": 0.,  # costs of all shares in RUB
1556                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1557                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1558                "futuresCostRUB": 0.,  # costs of all futures in RUB
1559                "Currencies": [],  # list of dictionaries of all currencies statistics
1560                "Shares": [],  # list of dictionaries of all shares statistics
1561                "Bonds": [],  # list of dictionaries of all bonds statistics
1562                "Etfs": [],  # list of dictionaries of all etfs statistics
1563                "Futures": [],  # list of dictionaries of all futures statistics
1564                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1565                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1566                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1567                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1568                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1569            },
1570            "analytics": {  # --- some analytics of portfolio:
1571                "distrByAssets": {},  # portfolio distribution by assets
1572                "distrByCompanies": {},  # portfolio distribution by companies
1573                "distrBySectors": {},  # portfolio distribution by sectors
1574                "distrByCurrencies": {},  # portfolio distribution by currencies
1575                "distrByCountries": {},  # portfolio distribution by countries
1576                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1577            }
1578        }
1579
1580        details = details.lower()
1581        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1582        if details not in availableDetails:
1583            details = "full"
1584            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1585
1586        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1587
1588        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1589        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1590        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending orders (list)
1591        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1592
1593        # save response headers without "positions" section:
1594        for key in portfolioResponse.keys():
1595            if key != "positions":
1596                view["raw"]["headers"][key] = portfolioResponse[key]
1597
1598            else:
1599                continue
1600
1601        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1602        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1603        for item in portfolioResponse["positions"]:
1604            if item["instrumentType"] == "currency":
1605                self.figi = item["figi"]
1606                curr = self.SearchByFIGI(requestPrice=False)
1607
1608                # current price of currency in RUB:
1609                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1610                    "name": curr["name"],
1611                    "currentPrice": NanoToFloat(
1612                        item["currentPrice"]["units"],
1613                        item["currentPrice"]["nano"]
1614                    ),
1615                }
1616
1617                view["raw"]["Currencies"].append(item)
1618
1619            elif item["instrumentType"] == "share":
1620                view["raw"]["Shares"].append(item)
1621
1622            elif item["instrumentType"] == "bond":
1623                view["raw"]["Bonds"].append(item)
1624
1625            elif item["instrumentType"] == "etf":
1626                view["raw"]["Etfs"].append(item)
1627
1628            elif item["instrumentType"] == "futures":
1629                view["raw"]["Futures"].append(item)
1630
1631            else:
1632                continue
1633
1634        # how many volume of currencies (by ISO currency name) are blocked:
1635        for item in view["raw"]["positions"]["blocked"]:
1636            blocked = NanoToFloat(item["units"], item["nano"])
1637            if blocked > 0:
1638                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1639
1640        # how many volume of instruments (by FIGI) are blocked:
1641        for item in view["raw"]["positions"]["securities"]:
1642            blocked = int(item["blocked"])
1643            if blocked > 0:
1644                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1645
1646        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1647
1648        if "rub" in allBlocked.keys():
1649            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1650
1651        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1652        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1653        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1654        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1655        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1656        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1657        view["stat"]["portfolioCostRUB"] = sum([
1658            view["stat"]["allCurrenciesCostRUB"],
1659            view["stat"]["sharesCostRUB"],
1660            view["stat"]["bondsCostRUB"],
1661            view["stat"]["etfsCostRUB"],
1662            view["stat"]["futuresCostRUB"],
1663        ])
1664
1665        # --- calculating some portfolio statistics:
1666        byComp = {}  # distribution by companies
1667        bySect = {}  # distribution by sectors
1668        byCurr = {}  # distribution by currencies (include RUB)
1669        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1670        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1671
1672        for item in portfolioResponse["positions"]:
1673            self.figi = item["figi"]
1674            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1675
1676            if instrument:
1677                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1678                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1679
1680                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1681                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1682
1683                else:
1684                    blocked = 0
1685
1686                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1687                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1688                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1689                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1690                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1691                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1692                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1693                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1694                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1695                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1696                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1697                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1698
1699                statData = {
1700                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1701                    "ticker": instrument["ticker"],  # ticker by FIGI
1702                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1703                    "volume": volume,  # available volume of instrument
1704                    "lots": lots,  # volume in lots of instrument
1705                    "direction": direction,  # direction of an instrument's position: short or long
1706                    "blocked": blocked,  # blocked volume of currency or instrument
1707                    "currentPrice": curPrice,  # current instrument's price in basic asset
1708                    "average": average,  # current average position price
1709                    "cost": cost,  # current cost of all volume of instrument in basic asset
1710                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1711                    "costRUB": costRUB,  # cost of instrument in ruble
1712                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1713                    "profit": profit,  # expected profit at current moment
1714                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1715                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1716                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1717                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1718                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1719                    "step": instrument["step"],  # minimum price increment
1720                }
1721
1722                # adding distribution by unique countries:
1723                if statData["country"] not in byCountry.keys():
1724                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1725
1726                else:
1727                    byCountry[statData["country"]]["cost"] += costRUB
1728                    byCountry[statData["country"]]["percent"] += percentCostRUB
1729
1730                if item["instrumentType"] != "currency":
1731                    # adding distribution by unique companies:
1732                    if statData["name"]:
1733                        if statData["name"] not in byComp.keys():
1734                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1735
1736                        else:
1737                            byComp[statData["name"]]["cost"] += costRUB
1738                            byComp[statData["name"]]["percent"] += percentCostRUB
1739
1740                    # adding distribution by unique sectors:
1741                    if statData["sector"] not in bySect.keys():
1742                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1743
1744                    else:
1745                        bySect[statData["sector"]]["cost"] += costRUB
1746                        bySect[statData["sector"]]["percent"] += percentCostRUB
1747
1748                # adding distribution by unique currencies:
1749                if currency not in byCurr.keys():
1750                    byCurr[currency] = {
1751                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1752                        "cost": costRUB,
1753                        "percent": percentCostRUB
1754                    }
1755
1756                else:
1757                    byCurr[currency]["cost"] += costRUB
1758                    byCurr[currency]["percent"] += percentCostRUB
1759
1760                # saving statistics for every instrument:
1761                if item["instrumentType"] == "currency":
1762                    view["stat"]["Currencies"].append(statData)
1763
1764                    # update dict with free funds for trading (total - blocked) by currencies
1765                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1766                    view["stat"]["funds"][currency] = {
1767                        "total": volume,
1768                        "totalCostRUB": costRUB,  # total volume cost in rubles
1769                        "free": volume - blocked,
1770                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1771                    }
1772
1773                elif item["instrumentType"] == "share":
1774                    view["stat"]["Shares"].append(statData)
1775
1776                elif item["instrumentType"] == "bond":
1777                    view["stat"]["Bonds"].append(statData)
1778
1779                elif item["instrumentType"] == "etf":
1780                    view["stat"]["Etfs"].append(statData)
1781
1782                elif item["instrumentType"] == "Futures":
1783                    view["stat"]["Futures"].append(statData)
1784
1785                else:
1786                    continue
1787
1788        # total changes in Russian Ruble:
1789        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1790        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1791        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1792        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1793        view["stat"]["funds"]["rub"] = {
1794            "total": view["stat"]["availableRUB"],
1795            "totalCostRUB": view["stat"]["availableRUB"],
1796            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1797            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1798        }
1799
1800        # --- pending orders sector data:
1801        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending orders to avoid many times price requests
1802        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1803
1804        for item in view["raw"]["orders"]:
1805            self.figi = item["figi"]
1806
1807            if item["figi"] not in uniquePendingOrdersFIGIs:
1808                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1809
1810                uniquePendingOrdersFIGIs.append(item["figi"])
1811                uniquePendingOrders[item["figi"]] = instrument
1812
1813            else:
1814                instrument = uniquePendingOrders[item["figi"]]
1815
1816            if instrument:
1817                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1818                orderType = TKS_ORDER_TYPES[item["orderType"]]
1819                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1820                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1821
1822                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1823                if item["direction"] == "ORDER_DIRECTION_BUY":
1824                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1825
1826                else:
1827                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1828
1829                # requested price for order execution:
1830                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1831
1832                # necessary changes in percent to reach target from current price:
1833                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1834
1835                view["stat"]["orders"].append({
1836                    "orderID": item["orderId"],  # orderId number parameter of current order
1837                    "figi": item["figi"],  # FIGI identification
1838                    "ticker": instrument["ticker"],  # ticker name by FIGI
1839                    "lotsRequested": item["lotsRequested"],  # requested lots value
1840                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1841                    "currentPrice": lastPrice,  # current instrument's price for defined action
1842                    "targetPrice": target,  # requested price for order execution in base currency
1843                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1844                    "percentChanges": changes,  # changes in percent to target from current price
1845                    "currency": item["currency"],  # instrument's currency name
1846                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1847                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1848                    "status": orderState,  # order status from TKS_ORDER_STATES
1849                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1850                })
1851
1852        # --- stop orders sector data:
1853        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1854        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1855
1856        for item in view["raw"]["stopOrders"]:
1857            self.figi = item["figi"]
1858
1859            if item["figi"] not in uniqueStopOrdersFIGIs:
1860                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1861
1862                uniqueStopOrdersFIGIs.append(item["figi"])
1863                uniqueStopOrders[item["figi"]] = instrument
1864
1865            else:
1866                instrument = uniqueStopOrders[item["figi"]]
1867
1868            if instrument:
1869                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1870                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1871                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1872
1873                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1874                if "expirationTime" in item.keys():
1875                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1876                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1877
1878                else:
1879                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1880                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1881
1882                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1883                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1884                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1885
1886                else:
1887                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1888
1889                # requested price when stop-order executed:
1890                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1891
1892                # price for limit-order, set up when stop-order executed:
1893                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1894
1895                # necessary changes in percent to reach target from current price:
1896                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1897
1898                view["stat"]["stopOrders"].append({
1899                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
1900                    "figi": item["figi"],  # FIGI identification
1901                    "ticker": instrument["ticker"],  # ticker name by FIGI
1902                    "lotsRequested": item["lotsRequested"],  # requested lots value
1903                    "currentPrice": lastPrice,  # current instrument's price for defined action
1904                    "targetPrice": target,  # requested price for stop-order execution in base currency
1905                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
1906                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
1907                    "percentChanges": changes,  # changes in percent to target from current price
1908                    "currency": item["currency"],  # instrument's currency name
1909                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
1910                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
1911                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
1912                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
1913                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
1914                })
1915
1916        # --- calculating data for analytics section:
1917        # portfolio distribution by assets:
1918        view["analytics"]["distrByAssets"] = {
1919            "Ruble": {
1920                "uniques": 1,
1921                "cost": view["stat"]["availableRUB"],
1922                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1923            },
1924            "Currencies": {
1925                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
1926                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
1927                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1928            },
1929            "Shares": {
1930                "uniques": len(view["stat"]["Shares"]),
1931                "cost": view["stat"]["sharesCostRUB"],
1932                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1933            },
1934            "Bonds": {
1935                "uniques": len(view["stat"]["Bonds"]),
1936                "cost": view["stat"]["bondsCostRUB"],
1937                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1938            },
1939            "Etfs": {
1940                "uniques": len(view["stat"]["Etfs"]),
1941                "cost": view["stat"]["etfsCostRUB"],
1942                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1943            },
1944            "Futures": {
1945                "uniques": len(view["stat"]["Futures"]),
1946                "cost": view["stat"]["futuresCostRUB"],
1947                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1948            },
1949        }
1950
1951        # portfolio distribution by companies:
1952        view["analytics"]["distrByCompanies"]["All money cash"] = {
1953            "ticker": "",
1954            "cost": view["stat"]["allCurrenciesCostRUB"],
1955            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1956        }
1957        view["analytics"]["distrByCompanies"].update(byComp)
1958
1959        # portfolio distribution by sectors:
1960        view["analytics"]["distrBySectors"]["All money cash"] = {
1961            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
1962            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
1963        }
1964        view["analytics"]["distrBySectors"].update(bySect)
1965
1966        # portfolio distribution by currencies:
1967        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
1968            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
1969
1970            if self.moreDebug:
1971                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
1972
1973        view["analytics"]["distrByCurrencies"].update(byCurr)
1974        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
1975        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
1976
1977        # portfolio distribution by countries:
1978        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
1979            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
1980
1981            if self.moreDebug:
1982                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
1983
1984        view["analytics"]["distrByCountries"].update(byCountry)
1985        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
1986        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
1987
1988        # --- Prepare text statistics overview in human-readable:
1989        if show:
1990            # Whatever the value `details`, header not changes:
1991            info = [
1992                "# Client's portfolio\n\n",
1993                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1994                "* **Account ID:** [{}]\n".format(self.accountId),
1995            ]
1996
1997            if details in ["full", "positions", "digest"]:
1998                info.extend([
1999                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2000                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2001                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2002                        view["stat"]["totalChangesRUB"],
2003                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2004                        view["stat"]["totalChangesPercentRUB"],
2005                    ),
2006                ])
2007
2008            if details in ["full", "positions"]:
2009                info.extend([
2010                    "## Open positions\n\n",
2011                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2012                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2013                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2014                        "{:.2f} ({:.2f}) rub".format(
2015                            view["stat"]["availableRUB"],
2016                            view["stat"]["blockedRUB"],
2017                        )
2018                    )
2019                ])
2020
2021                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2022                    return [
2023                        "|                             |                                 |          |              |              |                     |                              |\n",
2024                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2025                            noTradeStr if noTradeStr else typeStr,
2026                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2027                        ),
2028                    ]
2029
2030                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2031                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2032                        "{} [{}]".format(data["ticker"], data["figi"]),
2033                        "{:.2f} ({:.2f}) {}".format(
2034                            data["volume"],
2035                            data["blocked"],
2036                            data["currency"],
2037                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2038                            data["volume"],
2039                            data["blocked"],
2040                        ),
2041                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2042                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2043                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2044                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2045                        "{}{:.2f} {} ({}{:.2f}%)".format(
2046                            "+" if data["profit"] > 0 else "",
2047                            data["profit"], data["baseCurrencyName"],
2048                            "+" if data["percentProfit"] > 0 else "",
2049                            data["percentProfit"],
2050                        ),
2051                    )
2052
2053                # --- Show currencies section:
2054                if view["stat"]["Currencies"]:
2055                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2056                    for item in view["stat"]["Currencies"]:
2057                        info.append(_InfoStr(item, showCurrencyName=True))
2058
2059                else:
2060                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2061
2062                # --- Show shares section:
2063                if view["stat"]["Shares"]:
2064                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2065
2066                    for item in view["stat"]["Shares"]:
2067                        info.append(_InfoStr(item))
2068
2069                else:
2070                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2071
2072                # --- Show bonds section:
2073                if view["stat"]["Bonds"]:
2074                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2075
2076                    for item in view["stat"]["Bonds"]:
2077                        info.append(_InfoStr(item))
2078
2079                else:
2080                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2081
2082                # --- Show etfs section:
2083                if view["stat"]["Etfs"]:
2084                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2085
2086                    for item in view["stat"]["Etfs"]:
2087                        info.append(_InfoStr(item))
2088
2089                else:
2090                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2091
2092                # --- Show futures section:
2093                if view["stat"]["Futures"]:
2094                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2095
2096                    for item in view["stat"]["Futures"]:
2097                        info.append(_InfoStr(item))
2098
2099                else:
2100                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2101
2102            if details in ["full", "orders"]:
2103                # --- Show pending orders section:
2104                if view["stat"]["orders"]:
2105                    info.extend([
2106                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2107                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2108                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2109                    ])
2110
2111                    for item in view["stat"]["orders"]:
2112                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2113                            "{} [{}]".format(item["ticker"], item["figi"]),
2114                            item["orderID"],
2115                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2116                            "{} {} ({}{:.2f}%)".format(
2117                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2118                                item["baseCurrencyName"],
2119                                "+" if item["percentChanges"] > 0 else "",
2120                                float(item["percentChanges"]),
2121                            ),
2122                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2123                            item["action"],
2124                            item["type"],
2125                            item["date"],
2126                        ))
2127
2128                else:
2129                    info.append("\n## Total pending limit-orders: 0\n")
2130
2131                # --- Show stop orders section:
2132                if view["stat"]["stopOrders"]:
2133                    info.extend([
2134                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2135                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2136                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2137                    ])
2138
2139                    for item in view["stat"]["stopOrders"]:
2140                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2141                            "{} [{}]".format(item["ticker"], item["figi"]),
2142                            item["orderID"],
2143                            item["lotsRequested"],
2144                            "{} {} ({}{:.2f}%)".format(
2145                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2146                                item["baseCurrencyName"],
2147                                "+" if item["percentChanges"] > 0 else "",
2148                                float(item["percentChanges"]),
2149                            ),
2150                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2151                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2152                            item["action"],
2153                            item["type"],
2154                            item["expType"],
2155                            item["createDate"],
2156                            item["expDate"],
2157                        ))
2158
2159                else:
2160                    info.append("\n## Total stop-orders: 0\n")
2161
2162            if details in ["full", "analytics"]:
2163                # -- Show analytics section:
2164                if view["stat"]["portfolioCostRUB"] > 0:
2165                    info.extend([
2166                        "\n# Analytics\n"
2167                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2168                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2169                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2170                            view["stat"]["totalChangesRUB"],
2171                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2172                            view["stat"]["totalChangesPercentRUB"],
2173                        ),
2174                        "\n## Portfolio distribution by assets\n"
2175                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2176                        "|------------------------------------|---------|---------|--------------------|\n",
2177                    ])
2178
2179                    for key in view["analytics"]["distrByAssets"].keys():
2180                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2181                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2182                                key,
2183                                view["analytics"]["distrByAssets"][key]["uniques"],
2184                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2185                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2186                            ))
2187
2188                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2189
2190                    info.extend([
2191                        "\n## Portfolio distribution by companies\n"
2192                        "\n| Company                                      | Percent | Current cost       |\n",
2193                        aSepLine,
2194                    ])
2195
2196                    for company in view["analytics"]["distrByCompanies"].keys():
2197                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2198                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2199                                "{}{}".format(
2200                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2201                                    company,
2202                                ),
2203                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2204                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2205                            ))
2206
2207                    info.extend([
2208                        "\n## Portfolio distribution by sectors\n"
2209                        "\n| Sector                                       | Percent | Current cost       |\n",
2210                        aSepLine,
2211                    ])
2212
2213                    for sector in view["analytics"]["distrBySectors"].keys():
2214                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2215                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2216                                sector,
2217                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2218                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2219                            ))
2220
2221                    info.extend([
2222                        "\n## Portfolio distribution by currencies\n"
2223                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2224                        aSepLine,
2225                    ])
2226
2227                    for curr in view["analytics"]["distrByCurrencies"].keys():
2228                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2229                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2230                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2231                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2232                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2233                            ))
2234
2235                    info.extend([
2236                        "\n## Portfolio distribution by countries\n"
2237                        "\n| Assets by country                            | Percent | Current cost       |\n",
2238                        aSepLine,
2239                    ])
2240
2241                    for country in view["analytics"]["distrByCountries"].keys():
2242                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2243                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2244                                country,
2245                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2246                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2247                            ))
2248
2249            if details in ["full", "calendar"]:
2250                # -- Show bonds payment calendar section:
2251                if view["stat"]["Bonds"]:
2252                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2253                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2254                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2255
2256                else:
2257                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2258
2259            infoText = "".join(info)
2260
2261            uLogger.info(infoText)
2262
2263            if details == "full" and self.overviewFile:
2264                filename = self.overviewFile
2265
2266            elif details == "digest" and self.overviewDigestFile:
2267                filename = self.overviewDigestFile
2268
2269            elif details == "positions" and self.overviewPositionsFile:
2270                filename = self.overviewPositionsFile
2271
2272            elif details == "orders" and self.overviewOrdersFile:
2273                filename = self.overviewOrdersFile
2274
2275            elif details == "analytics" and self.overviewAnalyticsFile:
2276                filename = self.overviewAnalyticsFile
2277
2278            elif details == "calendar" and self.overviewBondsCalendarFile:
2279                filename = self.overviewBondsCalendarFile
2280
2281            else:
2282                filename = ""
2283
2284            if filename:
2285                with open(filename, "w", encoding="UTF-8") as fH:
2286                    fH.write(infoText)
2287
2288                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2289
2290        return view
2291
2292    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2293        """
2294        Returns history operations between two given dates for current `accountId`.
2295        If `reportFile` string is not empty then also save human-readable report.
2296        Shows some statistical data of closed positions.
2297
2298        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2299        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2300        :param show: if `True` then also prints all records to the console.
2301        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2302        :return: original list of dictionaries with history of deals records from API ("operations" key):
2303                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2304                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2305        """
2306        if self.accountId is None or not self.accountId:
2307            uLogger.error("Variable `accountId` must be defined for using this method!")
2308            raise Exception("Account ID required")
2309
2310        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2311
2312        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2313
2314        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2315        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2316        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2317        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2318        customStat = {}  # custom statistics in additional to responseJSON
2319
2320        # --- output report in human-readable format:
2321        if show or self.reportFile:
2322            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2323            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2324            nextDay = ""
2325
2326            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2327
2328            if len(ops) > 0:
2329                customStat = {
2330                    "opsCount": 0,  # total operations count
2331                    "buyCount": 0,  # buy operations
2332                    "sellCount": 0,  # sell operations
2333                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2334                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2335                    "payIn": {"rub": 0.},  # Deposit brokerage account
2336                    "payOut": {"rub": 0.},  # Withdrawals
2337                    "divs": {"rub": 0.},  # Dividends income
2338                    "coupons": {"rub": 0.},  # Coupon's income
2339                    "brokerCom": {"rub": 0.},  # Service commissions
2340                    "serviceCom": {"rub": 0.},  # Service commissions
2341                    "marginCom": {"rub": 0.},  # Margin commissions
2342                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2343                }
2344
2345                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2346                for item in ops:
2347                    if item["state"] == "OPERATION_STATE_EXECUTED":
2348                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2349
2350                        # count buy operations:
2351                        if "_BUY" in item["operationType"]:
2352                            customStat["buyCount"] += 1
2353
2354                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2355                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2356
2357                            else:
2358                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2359
2360                        # count sell operations:
2361                        elif "_SELL" in item["operationType"]:
2362                            customStat["sellCount"] += 1
2363
2364                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2365                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2366
2367                            else:
2368                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2369
2370                        # count incoming operations:
2371                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2372                            if item["payment"]["currency"] in customStat["payIn"].keys():
2373                                customStat["payIn"][item["payment"]["currency"]] += payment
2374
2375                            else:
2376                                customStat["payIn"][item["payment"]["currency"]] = payment
2377
2378                        # count withdrawals operations:
2379                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2380                            if item["payment"]["currency"] in customStat["payOut"].keys():
2381                                customStat["payOut"][item["payment"]["currency"]] += payment
2382
2383                            else:
2384                                customStat["payOut"][item["payment"]["currency"]] = payment
2385
2386                        # count dividends income:
2387                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2388                            if item["payment"]["currency"] in customStat["divs"].keys():
2389                                customStat["divs"][item["payment"]["currency"]] += payment
2390
2391                            else:
2392                                customStat["divs"][item["payment"]["currency"]] = payment
2393
2394                        # count coupon's income:
2395                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2396                            if item["payment"]["currency"] in customStat["coupons"].keys():
2397                                customStat["coupons"][item["payment"]["currency"]] += payment
2398
2399                            else:
2400                                customStat["coupons"][item["payment"]["currency"]] = payment
2401
2402                        # count broker commissions:
2403                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2404                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2405                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2406
2407                            else:
2408                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2409
2410                        # count service commissions:
2411                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2412                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2413                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2414
2415                            else:
2416                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2417
2418                        # count margin commissions:
2419                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2420                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2421                                customStat["marginCom"][item["payment"]["currency"]] += payment
2422
2423                            else:
2424                                customStat["marginCom"][item["payment"]["currency"]] = payment
2425
2426                        # count withholding taxes:
2427                        elif "_TAX" in item["operationType"]:
2428                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2429                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2430
2431                            else:
2432                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2433
2434                        else:
2435                            continue
2436
2437                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2438
2439                # --- view "Actions" lines:
2440                info.extend([
2441                    "| Report sections            |                               |                              |                      |                        |\n",
2442                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2443                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2444                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2445                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2446                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2447                    ),
2448                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2449                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2450                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2451                    ),
2452                ])
2453
2454                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2455                for key in opsKeys:
2456                    if key == "rub":
2457                        continue
2458
2459                    info.extend([
2460                        "|                            |                               | {:<28} |                      |                        |\n".format(
2461                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2462                        ),
2463                        "|                            |                               | {:<28} |                      |                        |\n".format(
2464                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2465                        ),
2466                    ])
2467
2468                info.append(splitLine1)
2469
2470                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2471                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2472                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2473                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2474                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2475                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2476                    )
2477
2478                # --- view "Payments" lines:
2479                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2480                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2481
2482                for key in paymentsKeys:
2483                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2484
2485                info.append(splitLine1)
2486
2487                # --- view "Commissions and taxes" lines:
2488                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2489                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2490
2491                for key in comKeys:
2492                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2493
2494                info.append(splitLine1)
2495
2496                info.extend([
2497                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2498                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2499                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2500                ])
2501
2502            else:
2503                info.append("Broker returned no operations during this period\n")
2504
2505            # --- view "Operations" section:
2506            for item in ops:
2507                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2508                    continue
2509
2510                else:
2511                    self.figi = item["figi"] if item["figi"] else ""
2512                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2513                    instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {}
2514
2515                    # group of deals during one day:
2516                    if nextDay and item["date"].split("T")[0] != nextDay:
2517                        info.append(splitLine2)
2518                        nextDay = ""
2519
2520                    else:
2521                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2522
2523                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2524                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2525                        self.figi if self.figi else "—",
2526                        instrument["ticker"] if instrument else "—",
2527                        instrument["type"] if instrument else "—",
2528                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2529                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2530                        TKS_OPERATION_STATES[item["state"]],
2531                        TKS_OPERATION_TYPES[item["operationType"]],
2532                    ))
2533
2534            infoText = "".join(info)
2535
2536            if show:
2537                if self.moreDebug:
2538                    uLogger.debug("Records about history of a client's operations successfully received")
2539
2540                uLogger.info(infoText)
2541
2542            if self.reportFile:
2543                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2544                    fH.write(infoText)
2545
2546                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2547
2548        return ops, customStat
2549
2550    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2551        """
2552        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2553
2554        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2555        Warning! Broker server used ISO UTC time by default.
2556
2557        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2558        Also, `historyFile` used to update history with `onlyMissing` parameter.
2559
2560        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2561
2562        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2563        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2564        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2565                         `"hour"`, `"day"`. Default: `"hour"`.
2566        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2567                            False by default. Warning! History appends only from last candle to current time
2568                            with always update last candle!
2569        :param csvSep: separator if csv-file is used, `,` by default.
2570        :param show: if `True` then also prints Pandas DataFrame to the console.
2571        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2572                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2573        """
2574        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2575        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2576        history = None  # empty pandas object for history
2577
2578        if interval not in TKS_CANDLE_INTERVALS.keys():
2579            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2580            raise Exception("Incorrect value")
2581
2582        if not (self.ticker or self.figi):
2583            uLogger.error("Ticker or FIGI must be defined!")
2584            raise Exception("Ticker or FIGI required")
2585
2586        if self.ticker and not self.figi:
2587            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2588            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2589
2590        if self.figi and not self.ticker:
2591            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2592            self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2593
2594        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2595        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2596        if interval.lower() != "day":
2597            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2598
2599        delta = dtEnd - dtStart  # current UTC time minus last time in file
2600        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2601
2602        # calculate history length in candles:
2603        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2604        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2605            length += 1  # to avoid fraction time
2606
2607        # calculate data blocks count:
2608        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2609
2610        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2611        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2612        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2613        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2614        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi))
2615
2616        tempOld = None  # pandas object for old history, if --only-missing key present
2617        lastTime = None  # datetime object of last old candle in file
2618
2619        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2620            uLogger.debug("--only-missing key present, add only last missing candles...")
2621            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2622
2623            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2624
2625            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2626            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2627            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2628            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2629
2630            # get last datetime object from last string in file or minus 1 delta if file is empty:
2631            if len(tempOld) > 0:
2632                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2633
2634            else:
2635                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2636
2637            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2638
2639        responseJSONs = []  # raw history blocks of data
2640
2641        blockEnd = dtEnd
2642        for item in range(blocks):
2643            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2644            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2645
2646            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2647                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2648            ))
2649
2650            if blockStart == blockEnd:
2651                uLogger.debug("Skipped this zero-length block...")
2652
2653            else:
2654                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2655                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2656                self.body = str({
2657                    "figi": self.figi,
2658                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2659                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2660                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2661                })
2662                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2663
2664                if "code" in responseJSON.keys():
2665                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2666
2667                else:
2668                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2669                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2670
2671                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2672
2673            blockEnd = blockStart
2674
2675        printCount = len(responseJSONs)  # candles to show in console
2676        if responseJSONs:
2677            tempHistory = pd.DataFrame(
2678                data={
2679                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2680                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2681                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2682                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2683                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2684                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2685                    "volume": [int(item["volume"]) for item in responseJSONs],
2686                },
2687                index=range(len(responseJSONs)),
2688                columns=["date", "time", "open", "high", "low", "close", "volume"],
2689            )
2690            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2691            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2692
2693            # append only newest candles to old history if --only-missing key present:
2694            if onlyMissing and tempOld is not None and lastTime is not None:
2695                index = 0  # find start index in tempHistory data:
2696
2697                for i, item in tempHistory.iterrows():
2698                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2699
2700                    if curTime == lastTime:
2701                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2702                        index = i
2703                        printCount = index + 1
2704                        break
2705
2706                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2707
2708            else:
2709                history = tempHistory  # if no `--only-missing` key then load full data from server
2710
2711            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2712
2713        if history is not None and not history.empty:
2714            if show:
2715                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2716                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2717                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2718                ))
2719
2720        else:
2721            uLogger.warning("Received an empty candles history!")
2722
2723        if self.historyFile is not None:
2724            if history is not None and not history.empty:
2725                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2726                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile)))
2727
2728            else:
2729                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2730
2731        else:
2732            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2733
2734        return history
2735
2736    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2737        """
2738        Load candles history from csv-file and return Pandas DataFrame object.
2739
2740        See also: `History()` and `ShowHistoryChart()` methods.
2741
2742        :param filePath: path to csv-file to open.
2743        """
2744        loadedHistory = None  # init candles data object
2745
2746        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2747
2748        if os.path.exists(filePath):
2749            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2750
2751            tfStr = self.priceModel.FormattedDelta(
2752                self.priceModel.timeframe,
2753                "{days} days {hours}h {minutes}m {seconds}s",
2754            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2755                self.priceModel.timeframe,
2756                "{hours}h {minutes}m {seconds}s",
2757            )
2758
2759            if loadedHistory is not None and not loadedHistory.empty:
2760                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2761                    len(loadedHistory),
2762                    tfStr,
2763                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2764                )
2765
2766            else:
2767                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2768
2769        else:
2770            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2771
2772        return loadedHistory
2773
2774    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2775        """
2776        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2777
2778        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2779        Default: `index.html` (both for interact and non-interact candlesticks chart).
2780
2781        See also: `History()` and `LoadHistory()` methods.
2782
2783        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2784        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2785                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2786                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2787                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2788        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2789                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2790        """
2791        if isinstance(candles, str):
2792            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2793            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2794
2795        elif isinstance(candles, pd.DataFrame):
2796            self.priceModel.prices = candles  # set candles chain from variable
2797            self.priceModel.ticker = self.ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2798
2799            if "datetime" not in candles.columns:
2800                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2801
2802        else:
2803            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2804            raise Exception("Incorrect value")
2805
2806        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2807
2808        if interact:
2809            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2810
2811            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2812
2813        else:
2814            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2815
2816            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2817
2818        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2819
2820    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2821        """
2822        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2823        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2824
2825        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2826
2827        :param operation: string "Buy" or "Sell".
2828        :param lots: volume, integer count of lots >= 1.
2829        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2830        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2831        :param expDate: string "Undefined" by default or local date in future,
2832                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2833        :return: JSON with response from broker server.
2834        """
2835        if self.accountId is None or not self.accountId:
2836            uLogger.error("Variable `accountId` must be defined for using this method!")
2837            raise Exception("Account ID required")
2838
2839        if operation is None or not operation or operation not in ("Buy", "Sell"):
2840            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2841            raise Exception("Incorrect value")
2842
2843        if lots is None or lots < 1:
2844            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2845            lots = 1
2846
2847        if tp is None or tp < 0:
2848            tp = 0
2849
2850        if sl is None or sl < 0:
2851            sl = 0
2852
2853        if expDate is None or not expDate:
2854            expDate = "Undefined"
2855
2856        if not (self.ticker or self.figi):
2857            uLogger.error("Ticker or FIGI must be defined!")
2858            raise Exception("Ticker or FIGI required")
2859
2860        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
2861        self.ticker = instrument["ticker"]
2862        self.figi = instrument["figi"]
2863
2864        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate))
2865
2866        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2867        self.body = str({
2868            "figi": self.figi,
2869            "quantity": str(lots),
2870            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2871            "accountId": str(self.accountId),
2872            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2873        })
2874        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
2875
2876        if "orderId" in response.keys():
2877            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2878                operation, response["orderId"],
2879                self.ticker, self.figi, lots,
2880                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2881                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2882                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2883            ))
2884
2885            if tp > 0:
2886                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2887
2888            if sl > 0:
2889                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2890
2891        else:
2892            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.")
2893
2894        return response
2895
2896    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2897        """
2898        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
2899        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
2900
2901        See also: `Order()` and `Trade()` docstrings.
2902
2903        :param lots: volume, integer count of lots >= 1.
2904        :param tp: float > 0, take profit price of stop-order.
2905        :param sl: float > 0, stop loss price of stop-order.
2906        :param expDate: it's a local date in future.
2907                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2908        :return: JSON with response from broker server.
2909        """
2910        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
2911
2912    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2913        """
2914        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
2915        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2916
2917        See also: `Order()` and `Trade()` docstrings.
2918
2919        :param lots: volume, integer count of lots >= 1.
2920        :param tp: float > 0, take profit price of stop-order.
2921        :param sl: float > 0, stop loss price of stop-order.
2922        :param expDate: it's a local date in the future.
2923                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2924        :return: JSON with response from broker server.
2925        """
2926        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
2927
2928    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
2929        """
2930        Close position of given instruments.
2931
2932        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
2933        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
2934                         This avoids unnecessary downloading data from the server.
2935        """
2936        if instruments is None or not instruments:
2937            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
2938            raise Exception("Ticker or FIGI required")
2939
2940        if isinstance(instruments, str):
2941            instruments = [instruments]
2942
2943        uniqueInstruments = self.GetUniqueFIGIs(instruments)
2944        if uniqueInstruments:
2945            if portfolio is None or not portfolio:
2946                portfolio = self.Overview(show=False)
2947
2948            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
2949            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
2950
2951            for self.figi in uniqueInstruments:
2952                if self.figi not in allOpened:
2953                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi))
2954                    continue
2955
2956                # search open trade info about instrument by ticker:
2957                instrument = {}
2958                for iType in TKS_INSTRUMENTS:
2959                    if instrument:
2960                        break
2961
2962                    for item in portfolio["stat"][iType]:
2963                        if item["figi"] == self.figi:
2964                            instrument = item
2965                            break
2966
2967                if instrument:
2968                    self.ticker = instrument["ticker"]
2969                    self.figi = instrument["figi"]
2970
2971                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
2972                        self.ticker,
2973                        self.figi,
2974                        int(instrument["volume"]),
2975                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
2976                    ))
2977
2978                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
2979
2980                    if tradeLots > 0:
2981                        if instrument["blocked"] > 0:
2982                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
2983                                instrument["blocked"],
2984                                self.ticker,
2985                                tradeLots,
2986                            ))
2987
2988                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
2989                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
2990
2991                    else:
2992                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
2993
2994    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
2995        """
2996        Close all positions of given instruments with defined type.
2997
2998        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
2999        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3000                         This avoids unnecessary downloading data from the server.
3001        """
3002        if iType not in TKS_INSTRUMENTS:
3003            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3004
3005        else:
3006            if portfolio is None or not portfolio:
3007                portfolio = self.Overview(show=False)
3008
3009            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3010            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3011
3012            if tickers and portfolio:
3013                self.CloseTrades(tickers, portfolio)
3014
3015            else:
3016                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3017
3018    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3019        """
3020        Universal method to create market or limit orders with all available parameters for current `accountId`.
3021        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3022
3023        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3024        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3025
3026        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3027        then broker immediately open market order as you can do simple --buy or --sell operations!
3028
3029        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3030        When current price will go up or down to target price value then broker opens a limit order.
3031        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3032
3033        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3034
3035        :param operation: string "Buy" or "Sell".
3036        :param orderType: string "Limit" or "Stop".
3037        :param lots: volume, integer count of lots >= 1.
3038        :param targetPrice: target price > 0. This is open trade price for limit order.
3039        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3040                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3041        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3042                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3043                         Stop loss order always executed by market price.
3044        :param expDate: string "Undefined" by default or local date in future.
3045                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3046                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3047                        A limit order has no expiration date, it lasts until the end of the trading day.
3048        :return: JSON with response from broker server.
3049        """
3050        if self.accountId is None or not self.accountId:
3051            uLogger.error("Variable `accountId` must be defined for using this method!")
3052            raise Exception("Account ID required")
3053
3054        if operation is None or not operation or operation not in ("Buy", "Sell"):
3055            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3056            raise Exception("Incorrect value")
3057
3058        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3059            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3060            raise Exception("Incorrect value")
3061
3062        if lots is None or lots < 1:
3063            uLogger.error("You must define trade volume > 0: integer count of lots!")
3064            raise Exception("Incorrect value")
3065
3066        if targetPrice is None or targetPrice <= 0:
3067            uLogger.error("Target price for limit-order must be greater than 0!")
3068            raise Exception("Incorrect value")
3069
3070        if limitPrice is None or limitPrice <= 0:
3071            limitPrice = targetPrice
3072
3073        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3074            stopType = "Limit"
3075
3076        if expDate is None or not expDate:
3077            expDate = "Undefined"
3078
3079        if not (self.ticker or self.figi):
3080            uLogger.error("Tocker or FIGI must be defined!")
3081            raise Exception("Ticker or FIGI required")
3082
3083        response = {}
3084        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
3085        self.ticker = instrument["ticker"]
3086        self.figi = instrument["figi"]
3087
3088        if orderType == "Limit":
3089            uLogger.debug(
3090                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3091                    self.ticker, self.figi,
3092                    operation, lots, targetPrice, instrument["currency"],
3093                ))
3094
3095            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3096            self.body = str({
3097                "figi": self.figi,
3098                "quantity": str(lots),
3099                "price": FloatToNano(targetPrice),
3100                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3101                "accountId": str(self.accountId),
3102                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3103            })
3104            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3105
3106            if "orderId" in response.keys():
3107                uLogger.info(
3108                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3109                        response["orderId"],
3110                        self.ticker, self.figi,
3111                        operation, lots, targetPrice, instrument["currency"],
3112                    ))
3113
3114                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3115                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3116                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3117                            targetPrice, instrument["currency"],
3118                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3119                        ))
3120
3121                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3122                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3123                            targetPrice, instrument["currency"],
3124                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3125                        ))
3126
3127            else:
3128                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.")
3129
3130        if orderType == "Stop":
3131            uLogger.debug(
3132                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3133                    self.ticker, self.figi,
3134                    operation, lots,
3135                    targetPrice, instrument["currency"],
3136                    limitPrice, instrument["currency"],
3137                    stopType, expDate,
3138                ))
3139
3140            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3141            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3142            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3143
3144            body = {
3145                "figi": self.figi,
3146                "quantity": str(lots),
3147                "price": FloatToNano(limitPrice),
3148                "stopPrice": FloatToNano(targetPrice),
3149                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3150                "accountId": str(self.accountId),
3151                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3152                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3153            }
3154
3155            if expDateUTC:
3156                body["expireDate"] = expDateUTC
3157
3158            self.body = str(body)
3159            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3160
3161            if "stopOrderId" in response.keys():
3162                uLogger.info(
3163                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3164                        response["stopOrderId"],
3165                        self.ticker, self.figi,
3166                        operation, lots,
3167                        targetPrice, instrument["currency"],
3168                        limitPrice, instrument["currency"],
3169                        TKS_STOP_ORDER_TYPES[stopOrderType],
3170                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3171                    ))
3172
3173                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3174                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3175                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3176                            targetPrice, instrument["currency"],
3177                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3178                        ))
3179
3180                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3181                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3182                            targetPrice, instrument["currency"],
3183                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3184                        ))
3185
3186            else:
3187                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.")
3188
3189        return response
3190
3191    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3192        """
3193        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3194        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3195        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3196        See also: `Order()` docstring.
3197
3198        :param lots: volume, integer count of lots >= 1.
3199        :param targetPrice: target price > 0. This is open trade price for limit order.
3200        :return: JSON with response from broker server.
3201        """
3202        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3203
3204    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3205        """
3206        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3207        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3208        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3209        target price value then broker opens a limit order. See also: `Order()` docstring.
3210
3211        :param lots: volume, integer count of lots >= 1.
3212        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3213        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3214                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3215        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3216                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3217        :param expDate: string "Undefined" by default or local date in future.
3218                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3219                        This date is converting to UTC format for server.
3220        :return: JSON with response from broker server.
3221        """
3222        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3223
3224    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3225        """
3226        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3227        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3228        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3229        See also: `Order()` docstring.
3230
3231        :param lots: volume, integer count of lots >= 1.
3232        :param targetPrice: target price > 0. This is open trade price for limit order.
3233        :return: JSON with response from broker server.
3234        """
3235        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3236
3237    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3238        """
3239        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3240        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3241        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3242        target price value then broker opens a limit order. See also: `Order()` docstring.
3243
3244        :param lots: volume, integer count of lots >= 1.
3245        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3246        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3247                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3248        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3249                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3250        :param expDate: string "Undefined" by default or local date in future.
3251                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3252                        This date is converting to UTC format for server.
3253        :return: JSON with response from broker server.
3254        """
3255        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3256
3257    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3258        """
3259        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3260
3261        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3262        :param allOrdersIDs: pre-received lists of all active pending orders.
3263                             This avoids unnecessary downloading data from the server.
3264        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3265        """
3266        if self.accountId is None or not self.accountId:
3267            uLogger.error("Variable `accountId` must be defined for using this method!")
3268            raise Exception("Account ID required")
3269
3270        if orderIDs:
3271            if allOrdersIDs is None or not allOrdersIDs:
3272                rawOrders = self.RequestPendingOrders()
3273                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3274
3275            if allStopOrdersIDs is None or not allStopOrdersIDs:
3276                rawStopOrders = self.RequestStopOrders()
3277                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3278
3279            for orderID in orderIDs:
3280                idInPendingOrders = orderID in allOrdersIDs
3281                idInStopOrders = orderID in allStopOrdersIDs
3282
3283                if not (idInPendingOrders or idInStopOrders):
3284                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3285                    continue
3286
3287                else:
3288                    if idInPendingOrders:
3289                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3290
3291                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3292                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3293                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3294                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3295
3296                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3297                            if self.moreDebug:
3298                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3299
3300                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3301
3302                        else:
3303                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3304
3305                    elif idInStopOrders:
3306                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3307
3308                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3309                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3310                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3311                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3312
3313                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3314                            if self.moreDebug:
3315                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3316
3317                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3318
3319                        else:
3320                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3321
3322                    else:
3323                        continue
3324
3325    def CloseAllOrders(self) -> None:
3326        """
3327        Gets a list of open pending and stop orders and cancel it all.
3328        """
3329        rawOrders = self.RequestPendingOrders()
3330        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3331        lenOrders = len(allOrdersIDs)
3332
3333        rawStopOrders = self.RequestStopOrders()
3334        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3335        lenSOrders = len(allStopOrdersIDs)
3336
3337        if lenOrders > 0 or lenSOrders > 0:
3338            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3339
3340            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3341
3342        else:
3343            uLogger.info("Orders not found, nothing to cancel.")
3344
3345    def CloseAll(self, *args) -> None:
3346        """
3347        Close all available (not blocked) opened trades and orders.
3348
3349        Also, you can select one or more keywords case-insensitive:
3350        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3351
3352        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3353        """
3354        overview = self.Overview(show=False)  # get all open trades info
3355
3356        if len(args) == 0:
3357            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3358            self.CloseAllOrders()  # close all pending and stop orders
3359
3360            for iType in TKS_INSTRUMENTS:
3361                if iType != "Currencies":
3362                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3363
3364        else:
3365            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3366            lowerArgs = [x.lower() for x in args]
3367
3368            if "orders" in lowerArgs:
3369                self.CloseAllOrders()  # close all pending and stop orders
3370
3371            for iType in TKS_INSTRUMENTS:
3372                if iType.lower() in lowerArgs and iType != "Currencies":
3373                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3374
3375    @staticmethod
3376    def ParseOrderParameters(operation, **inputParameters):
3377        """
3378        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3379
3380        :param operation: string "Buy" or "Sell".
3381        :param inputParameters: this is dict of strings that looks like this
3382               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3383               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3384               "prices" key: one or more prices to open limit-orders
3385               Counts of values in lots and prices lists must be equals!
3386        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3387        """
3388        # TODO: update order grid work with api v2
3389        pass
3390        # uLogger.debug("Input parameters: {}".format(inputParameters))
3391        #
3392        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3393        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3394        #     raise Exception("Incorrect value")
3395        #
3396        # if "l" in inputParameters.keys():
3397        #     inputParameters["lots"] = inputParameters.pop("l")
3398        #
3399        # if "p" in inputParameters.keys():
3400        #     inputParameters["prices"] = inputParameters.pop("p")
3401        #
3402        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3403        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3404        #     raise Exception("Incorrect value")
3405        #
3406        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3407        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3408        #
3409        # if len(lots) != len(prices):
3410        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3411        #     raise Exception("Incorrect value")
3412        #
3413        # uLogger.debug("Extracted parameters for orders:")
3414        # uLogger.debug("lots = {}".format(lots))
3415        # uLogger.debug("prices = {}".format(prices))
3416        #
3417        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3418        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3419        # uLogger.debug("Order parameters: {}".format(result))
3420        #
3421        # return result
3422
3423    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3424        """
3425        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3426
3427        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3428        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3429        """
3430        result = False
3431        msg = "Instrument not defined!"
3432
3433        if portfolio is None or not portfolio:
3434            portfolio = self.Overview(show=False)
3435
3436        if self.ticker:
3437            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3438            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3439
3440            for iType in TKS_INSTRUMENTS:
3441                for instrument in portfolio["stat"][iType]:
3442                    if instrument["ticker"] == self.ticker:
3443                        result = True
3444                        msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker)
3445                        break
3446
3447        elif self.figi:
3448            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3449            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3450
3451            for iType in TKS_INSTRUMENTS:
3452                for instrument in portfolio["stat"][iType]:
3453                    if instrument["figi"] == self.figi:
3454                        result = True
3455                        msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi)
3456                        break
3457
3458        else:
3459            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3460
3461        uLogger.debug(msg)
3462
3463        return result
3464
3465    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3466        """
3467        Returns instrument from the user's portfolio if it presents there.
3468        Instrument must be defined by `ticker` (highly priority) or `figi`.
3469
3470        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3471        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3472        """
3473        result = None
3474        msg = "Instrument not defined!"
3475
3476        if portfolio is None or not portfolio:
3477            portfolio = self.Overview(show=False)
3478
3479        if self.ticker:
3480            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self.ticker))
3481            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3482
3483            for iType in TKS_INSTRUMENTS:
3484                for instrument in portfolio["stat"][iType]:
3485                    if instrument["ticker"] == self.ticker:
3486                        result = instrument
3487                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"])
3488                        break
3489
3490        elif self.figi:
3491            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3492            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3493
3494            for iType in TKS_INSTRUMENTS:
3495                for instrument in portfolio["stat"][iType]:
3496                    if instrument["figi"] == self.figi:
3497                        result = instrument
3498                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi)
3499                        break
3500
3501        else:
3502            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3503
3504        uLogger.debug(msg)
3505
3506        return result
3507
3508    def RequestLimits(self) -> dict:
3509        """
3510        Method for obtaining the available funds for withdrawal for current `accountId`.
3511
3512        See also:
3513        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3514        - `OverviewLimits()` method
3515
3516        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3517                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3518                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3519                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3520        """
3521        if self.accountId is None or not self.accountId:
3522            uLogger.error("Variable `accountId` must be defined for using this method!")
3523            raise Exception("Account ID required")
3524
3525        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3526
3527        self.body = str({"accountId": self.accountId})
3528        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3529        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3530
3531        if self.moreDebug:
3532            uLogger.debug("Records about available funds for withdrawal successfully received")
3533
3534        return rawLimits
3535
3536    def OverviewLimits(self, show: bool = False) -> dict:
3537        """
3538        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3539
3540        See also: `RequestLimits()`.
3541
3542        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3543        :return: dict with raw parsed data from server and some calculated statistics about it.
3544        """
3545        if self.accountId is None or not self.accountId:
3546            uLogger.error("Variable `accountId` must be defined for using this method!")
3547            raise Exception("Account ID required")
3548
3549        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3550
3551        view = {
3552            "rawLimits": rawLimits,
3553            "limits": {  # parsed data for every currency:
3554                "money": {  # this is an array of portfolio currency positions
3555                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3556                },
3557                "blocked": {  # this is an array of blocked currency
3558                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3559                },
3560                "blockedGuarantee": {  # this is locked money under collateral for futures
3561                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3562                },
3563            },
3564        }
3565
3566        # --- Prepare text table with limits in human-readable format:
3567        if show:
3568            info = [
3569                "# Withdrawal limits\n\n",
3570                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3571                "* **Account ID:** [{}]\n".format(self.accountId),
3572            ]
3573
3574            if view["limits"]["money"]:
3575                info.extend([
3576                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3577                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3578                ])
3579
3580            else:
3581                info.append("\nNo withdrawal limits\n")
3582
3583            for curr in view["limits"]["money"].keys():
3584                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3585                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3586                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3587
3588                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3589                    "[{}]".format(curr),
3590                    "{:.2f}".format(view["limits"]["money"][curr]),
3591                    "{:.2f}".format(availableMoney),
3592                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3593                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3594                )
3595
3596                if curr == "rub":
3597                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3598
3599                else:
3600                    info.append(infoStr)
3601
3602            infoText = "".join(info)
3603
3604            uLogger.info(infoText)
3605
3606            if self.withdrawalLimitsFile:
3607                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3608                    fH.write(infoText)
3609
3610                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3611
3612        return view
3613
3614    def RequestAccounts(self) -> dict:
3615        """
3616        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3617
3618        See also:
3619        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3620        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3621        - `OverviewUserInfo()` method
3622
3623        :return: dict with raw data from server that contains accounts info. Example of dict:
3624                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3625                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3626                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3627                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3628        """
3629        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3630
3631        self.body = str({})
3632        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3633        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3634
3635        if self.moreDebug:
3636            uLogger.debug("Records about available accounts successfully received")
3637
3638        return rawAccounts
3639
3640    def RequestUserInfo(self) -> dict:
3641        """
3642        Method for requesting common user's information.
3643
3644        See also:
3645        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3646        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3647        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3648        - `OverviewUserInfo()` method
3649
3650        :return: dict with raw data from server that contains user's information. Example of dict:
3651                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3652                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3653        """
3654        uLogger.debug("Requesting common user's information. Wait, please...")
3655
3656        self.body = str({})
3657        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3658        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3659
3660        if self.moreDebug:
3661            uLogger.debug("Records about current user successfully received")
3662
3663        return rawUserInfo
3664
3665    def RequestMarginStatus(self, accountId: str = None) -> dict:
3666        """
3667        Method for requesting margin calculation for defined account ID.
3668
3669        See also:
3670        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3671        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3672        - `OverviewUserInfo()` method
3673
3674        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3675        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3676                 Example of responses:
3677                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3678                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3679                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3680                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3681                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3682                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3683        """
3684        if accountId is None or not accountId:
3685            if self.accountId is None or not self.accountId:
3686                uLogger.error("Variable `accountId` must be defined for using this method!")
3687                raise Exception("Account ID required")
3688
3689            else:
3690                accountId = self.accountId  # use `self.accountId` (main ID) by default
3691
3692        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3693
3694        self.body = str({"accountId": accountId})
3695        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3696        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3697
3698        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3699            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3700            rawMargin = {}
3701
3702        else:
3703            if self.moreDebug:
3704                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3705
3706        return rawMargin
3707
3708    def RequestTariffLimits(self) -> dict:
3709        """
3710        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
3711
3712        See also:
3713        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
3714        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
3715        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
3716        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
3717        - `OverviewUserInfo()` method
3718
3719        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
3720                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
3721                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
3722        """
3723        uLogger.debug("Requesting limits of current tariff. Wait, please...")
3724
3725        self.body = str({})
3726        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
3727        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3728
3729        if self.moreDebug:
3730            uLogger.debug("Records with limits of current tariff successfully received")
3731
3732        return rawTariffLimits
3733
3734    def RequestBondCoupons(self, iJSON: dict) -> dict:
3735        """
3736        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
3737        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
3738        All dates are in UTC timezone.
3739
3740        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
3741        Documentation:
3742        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
3743        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
3744
3745        See also: `ExtendBondsData()`.
3746
3747        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]`
3748                      If raw iJSON is not data of bond then server returns an error [400] with message:
3749                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
3750        :return: dictionary with bond payment calendar. Response example
3751                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
3752                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
3753                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
3754                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
3755        """
3756        if iJSON["figi"] is None or not iJSON["figi"]:
3757            uLogger.error("FIGI must be defined for using this method!")
3758            raise Exception("FIGI required")
3759
3760        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
3761        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
3762
3763        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
3764            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
3765            self.figi,
3766            startDate,
3767            endDate,
3768        ))
3769
3770        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
3771        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
3772        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
3773
3774        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
3775            uLogger.warning("Instrument type is not bond!")
3776
3777        else:
3778            if self.moreDebug:
3779                uLogger.debug("Records about bond payment calendar successfully received")
3780
3781        return calendar
3782
3783    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
3784        """
3785        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
3786        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
3787        coupon yields, current yields and some statistics etc.
3788
3789        WARNING! This is too long operation if a lot of bonds requested from broker server.
3790
3791        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
3792
3793        :param instruments: list of strings with tickers or FIGIs.
3794        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
3795                     for further used by data scientists or stock analytics.
3796        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
3797                 In XLSX-file and Pandas DataFrame fields mean:
3798                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
3799                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
3800        """
3801        if instruments is None or not instruments:
3802            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3803            raise Exception("Ticker or FIGI required")
3804
3805        if isinstance(instruments, str):
3806            instruments = [instruments]
3807
3808        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3809
3810        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
3811
3812        iCount = len(uniqueInstruments)
3813        tooLong = iCount >= 20
3814        if tooLong:
3815            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
3816
3817        bonds = None
3818        for i, self.figi in enumerate(uniqueInstruments):
3819            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
3820
3821            if "type" in instrument.keys() and instrument["type"] == "Bonds":
3822                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
3823                rawBond = self.SearchByFIGI(requestPrice=True)
3824
3825                # Widen raw data with UTC current time (iData["actualDateTime"]):
3826                actualDate = datetime.now(tzutc())
3827                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
3828
3829                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
3830                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
3831
3832                # Replace some values with human-readable:
3833                iData["nominalCurrency"] = iData["nominal"]["currency"]
3834                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
3835                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
3836                iData["aciCurrency"] = iData["aciValue"]["currency"]
3837                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
3838                iData["issueSize"] = int(iData["issueSize"])
3839                iData["issueSizePlan"] = int(iData["issueSizePlan"])
3840                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
3841                iData["step"] = iData["step"] if "step" in iData.keys() else 0
3842                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
3843                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
3844                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
3845                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
3846                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
3847                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
3848                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
3849
3850                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
3851                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
3852                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
3853                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
3854                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
3855                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
3856                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
3857                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
3858                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
3859                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
3860                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
3861
3862                # Widen raw data with calendar data from `rawCalendar` values:
3863                calendarData = []
3864                if "events" in iData["rawCalendar"].keys():
3865                    for item in iData["rawCalendar"]["events"]:
3866                        calendarData.append({
3867                            "couponDate": item["couponDate"],
3868                            "couponNumber": int(item["couponNumber"]),
3869                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
3870                            "payCurrency": item["payOneBond"]["currency"],
3871                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
3872                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
3873                            "couponStartDate": item["couponStartDate"],
3874                            "couponEndDate": item["couponEndDate"],
3875                            "couponPeriod": item["couponPeriod"],
3876                        })
3877
3878                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
3879                    if "maturityDate" not in iData.keys():
3880                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
3881
3882                # Widen raw data with Coupon Rate.
3883                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
3884                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
3885                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
3886                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
3887
3888                # Widen raw data with Yield to Maturity (YTM) on current date.
3889                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
3890                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
3891                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
3892                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
3893                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
3894                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
3895
3896                iData["calendar"] = calendarData  # adds calendar at the end
3897
3898                # Remove not used data:
3899                iData.pop("uid")
3900                iData.pop("positionUid")
3901                iData.pop("currentPrice")
3902                iData.pop("rawCalendar")
3903
3904                colNames = list(iData.keys())
3905                if bonds is None:
3906                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
3907
3908                else:
3909                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
3910
3911            else:
3912                uLogger.warning("Instrument is not a bond!")
3913
3914            processed = round(100 * (i + 1) / iCount, 1)
3915            if tooLong and processed % 5 == 0:
3916                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
3917
3918            else:
3919                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
3920
3921        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
3922
3923        # Saving bonds from Pandas DataFrame to XLSX sheet:
3924        if xlsx and self.bondsXLSXFile:
3925            with pd.ExcelWriter(
3926                    path=self.bondsXLSXFile,
3927                    date_format=TKS_DATE_FORMAT,
3928                    datetime_format=TKS_DATE_TIME_FORMAT,
3929                    mode="w",
3930            ) as writer:
3931                bonds.to_excel(
3932                    writer,
3933                    sheet_name="Extended bonds data",
3934                    index=True,
3935                    encoding="UTF-8",
3936                    freeze_panes=(1, 1),
3937                )  # saving as XLSX-file with freeze first row and column as headers
3938
3939            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
3940
3941        return bonds
3942
3943    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
3944        """
3945        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
3946
3947        WARNING! This is too long operation if a lot of bonds requested from broker server.
3948
3949        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
3950
3951        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
3952                        extended information about bonds: main info, current prices, bond payment calendar,
3953                        coupon yields, current yields and some statistics etc.
3954                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
3955        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
3956                     for further used by data scientists or stock analytics.
3957        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
3958        """
3959        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
3960            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
3961
3962        uLogger.debug("Generating bond payments calendar data. Wait, please...")
3963
3964        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
3965        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
3966        calendar = None
3967        for bond in extBonds.iterrows():
3968            for item in bond[1]["calendar"]:
3969                cData = {
3970                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
3971                    "couponDate": item["couponDate"],
3972                    "figi": bond[1]["figi"],
3973                    "ticker": bond[1]["ticker"],
3974                    "name": bond[1]["name"],
3975                    "couponNumber": item["couponNumber"],
3976                    "payOneBond": item["payOneBond"],
3977                    "payCurrency": item["payCurrency"],
3978                    "couponType": item["couponType"],
3979                    "couponPeriod": item["couponPeriod"],
3980                    "fixDate": item["fixDate"],
3981                    "couponStartDate": item["couponStartDate"],
3982                    "couponEndDate": item["couponEndDate"],
3983                }
3984
3985                if calendar is None:
3986                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
3987
3988                else:
3989                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
3990
3991        if calendar is not None:
3992            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
3993
3994            # Saving calendar from Pandas DataFrame to XLSX sheet:
3995            if xlsx:
3996                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
3997
3998                with pd.ExcelWriter(
3999                        path=xlsxCalendarFile,
4000                        date_format=TKS_DATE_FORMAT,
4001                        datetime_format=TKS_DATE_TIME_FORMAT,
4002                        mode="w",
4003                ) as writer:
4004                    humanReadable = calendar.copy(deep=True)
4005                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4006                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4007                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4008                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4009                    humanReadable.columns = colNames  # human-readable column names
4010
4011                    humanReadable.to_excel(
4012                        writer,
4013                        sheet_name="Bond payments calendar",
4014                        index=False,
4015                        encoding="UTF-8",
4016                        freeze_panes=(1, 2),
4017                    )  # saving as XLSX-file with freeze first row and column as headers
4018
4019                    del humanReadable  # release df in memory
4020
4021                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4022
4023        return calendar
4024
4025    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4026        """
4027        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4028        Also, creates Markdown file with calendar data, `calendar.md` by default.
4029
4030        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4031
4032        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4033                        extended information about bonds: main info, current prices, bond payment calendar,
4034                        coupon yields, current yields and some statistics etc.
4035                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4036        :param show: if `True` then also printing bonds payment calendar to the console,
4037                     otherwise save to file `calendarFile` only. `False` by default.
4038        :return: multilines text in Markdown format with bonds payment calendar as a table.
4039        """
4040        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4041            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4042
4043        infoText = "# Bond payments calendar\n\n"
4044
4045        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4046
4047        if not (calendar is None or calendar.empty):
4048            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4049
4050            info = [
4051                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4052                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4053            ]
4054
4055            newMonth = False
4056            notOneBond = calendar["figi"].nunique() > 1
4057            for i, bond in enumerate(calendar.iterrows()):
4058                if newMonth and notOneBond:
4059                    info.append(splitLine)
4060
4061                info.append(
4062                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4063                        "  √" if bond[1]["paid"] else "  —",
4064                        bond[1]["couponDate"].split("T")[0],
4065                        bond[1]["figi"],
4066                        bond[1]["ticker"],
4067                        bond[1]["couponNumber"],
4068                        "{} {}".format(
4069                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4070                            bond[1]["payCurrency"],
4071                        ),
4072                        bond[1]["couponType"],
4073                        bond[1]["couponPeriod"],
4074                        bond[1]["fixDate"].split("T")[0],
4075                    )
4076                )
4077
4078                if i < len(calendar.values) - 1:
4079                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4080                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4081                    newMonth = False if curDate.month == nextDate.month else True
4082
4083                else:
4084                    newMonth = False
4085
4086            infoText += "".join(info)
4087
4088            if show:
4089                uLogger.info("{}".format(infoText))
4090
4091            if self.calendarFile is not None:
4092                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4093                    fH.write(infoText)
4094
4095                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4096
4097        else:
4098            infoText += "No data\n"
4099
4100        return infoText
4101
4102    def OverviewAccounts(self, show: bool = False) -> dict:
4103        """
4104        Method for parsing and show simple table with all available user accounts.
4105
4106        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4107
4108        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4109        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4110                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4111                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4112                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4113                                                        "closed": "—", "access": "Full access" }, ...}}`
4114        """
4115        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4116
4117        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4118        accounts = {
4119            item["id"]: {
4120                "type": TKS_ACCOUNT_TYPES[item["type"]],
4121                "name": item["name"],
4122                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4123                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4124                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4125                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4126            } for item in rawAccounts["accounts"]
4127        }
4128
4129        # Raw and parsed data with some fields replaced in "stat" section:
4130        view = {
4131            "rawAccounts": rawAccounts,
4132            "stat": accounts,
4133        }
4134
4135        # --- Prepare simple text table with only accounts data in human-readable format:
4136        if show:
4137            info = [
4138                "# User accounts\n\n",
4139                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4140                "| Account ID   | Type                      | Status                    | Name                           |\n",
4141                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4142            ]
4143
4144            for account in view["stat"].keys():
4145                info.extend([
4146                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4147                        account,
4148                        view["stat"][account]["type"],
4149                        view["stat"][account]["status"],
4150                        view["stat"][account]["name"],
4151                    )
4152                ])
4153
4154            infoText = "".join(info)
4155
4156            uLogger.info(infoText)
4157
4158            if self.userAccountsFile:
4159                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4160                    fH.write(infoText)
4161
4162                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4163
4164        return view
4165
4166    def OverviewUserInfo(self, show: bool = False) -> dict:
4167        """
4168        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4169
4170        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4171
4172        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4173        :return: dict with raw parsed data from server and some calculated statistics about it.
4174        """
4175        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4176        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4177        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4178        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4179        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4180        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4181
4182        # This is dict with parsed common user data:
4183        userInfo = {
4184            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4185            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4186            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4187            "tariff": rawUserInfo["tariff"],
4188        }
4189
4190        # This is an array of dict with parsed margin statuses for every account IDs:
4191        margins = {}
4192        for accountId in accounts.keys():
4193            if rawMargins[accountId]:
4194                margins[accountId] = {
4195                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4196                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4197                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4198                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4199                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4200                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4201                }
4202
4203            else:
4204                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4205
4206        unary = {}  # unary-connection limits
4207        for item in rawTariffLimits["unaryLimits"]:
4208            if item["limitPerMinute"] in unary.keys():
4209                unary[item["limitPerMinute"]].extend(item["methods"])
4210
4211            else:
4212                unary[item["limitPerMinute"]] = item["methods"]
4213
4214        stream = {}  # stream-connection limits
4215        for item in rawTariffLimits["streamLimits"]:
4216            if item["limit"] in stream.keys():
4217                stream[item["limit"]].extend(item["streams"])
4218
4219            else:
4220                stream[item["limit"]] = item["streams"]
4221
4222        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4223        limits = {
4224            "unary": unary,
4225            "stream": stream,
4226        }
4227
4228        # Raw and parsed data as an output result:
4229        view = {
4230            "rawUserInfo": rawUserInfo,
4231            "rawAccounts": rawAccounts,
4232            "rawMargins": rawMargins,
4233            "rawTariffLimits": rawTariffLimits,
4234            "stat": {
4235                "userInfo": userInfo,
4236                "accounts": accounts,
4237                "margins": margins,
4238                "limits": limits,
4239            },
4240        }
4241
4242        # --- Prepare text table with user information in human-readable format:
4243        if show:
4244            info = [
4245                "# Full user information\n\n",
4246                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4247                "## Common information\n\n",
4248                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4249                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4250                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4251                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4252                "\n## User accounts\n\n",
4253            ]
4254
4255            for account in view["stat"]["accounts"].keys():
4256                info.extend([
4257                    "### ID: [{}]\n\n".format(account),
4258                    "| Parameters           | Values                                                       |\n",
4259                    "|----------------------|--------------------------------------------------------------|\n",
4260                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4261                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4262                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4263                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4264                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4265                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4266                ])
4267
4268                if margins[account]:
4269                    info.extend([
4270                        "| Margin status:       | Enabled                                                      |\n",
4271                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4272                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4273                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4274                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4275                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4276                    ])
4277
4278                else:
4279                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4280
4281            info.extend([
4282                "\n## Current user tariff limits\n",
4283                "\nSee also:\n",
4284                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4285                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4286                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4287                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4288                "\n### Unary limits\n",
4289            ])
4290
4291            if unary:
4292                for key, values in sorted(unary.items()):
4293                    info.append("\n* Max requests per minute: {}\n".format(key))
4294
4295                    for value in values:
4296                        info.append("  - {}\n".format(value))
4297
4298            else:
4299                info.append("\nNot available\n")
4300
4301            info.append("\n### Stream limits\n")
4302
4303            if stream:
4304                for key, values in sorted(stream.items()):
4305                    info.append("\n* Max stream connections: {}\n".format(key))
4306
4307                    for value in values:
4308                        info.append("  - {}\n".format(value))
4309
4310            else:
4311                info.append("\nNot available\n")
4312
4313            infoText = "".join(info)
4314
4315            uLogger.info(infoText)
4316
4317            if self.userInfoFile:
4318                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4319                    fH.write(infoText)
4320
4321                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4322
4323        return view

This class implements methods to work with Tinkoff broker server.

Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/

About token: https://tinkoff.github.io/investAPI/token/

TinkoffBrokerServer( token: str, accountId: str = None, useCache: bool = True, defaultCache: str = 'dump.json')
 84    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
 85        """
 86        Main class init.
 87
 88        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
 89        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
 90                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
 91        :param useCache: use default cache file with raw data to use instead of `iList`.
 92                         True by default. Cache is auto-update if new day has come.
 93                         If you don't want to use cache and always updates raw data then set `useCache=False`.
 94        :param defaultCache: path to default cache file. `dump.json` by default.
 95        """
 96        if token is None or not token:
 97            try:
 98                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 99                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
100
101            except KeyError:
102                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
103                raise Exception("Token required")
104
105        else:
106            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
107            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
108
109        if accountId is None or not accountId:
110            try:
111                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
112                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
113
114            except KeyError:
115                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
116
117        else:
118            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
119            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
120
121        self.version = __version__  # duplicate here used TKSBrokerAPI main version
122        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
123
124        Latest version: https://pypi.org/project/tksbrokerapi/
125        """
126
127        self.aliases = TKS_TICKER_ALIASES
128        """Some aliases instead official tickers.
129
130        See also: `TKSEnums.TKS_TICKER_ALIASES`
131        """
132
133        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
134
135        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
136
137        self.ticker = ""
138        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
139
140        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
141        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
142
143        See also: `SearchByTicker()`, `SearchInstruments()`.
144        """
145
146        self.figi = ""
147        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
148
149        See also: `SearchByFIGI()`, `SearchInstruments()`.
150        """
151
152        self.depth = 1
153        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
154
155        See also: `GetCurrentPrices()`.
156        """
157
158        self.server = r"https://invest-public-api.tinkoff.ru/rest"
159        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
160
161        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
162        """
163
164        uLogger.debug("Broker API server: {}".format(self.server))
165
166        self.timeout = 15
167        """Server operations timeout in seconds. Default: `15`.
168
169        See also: `SendAPIRequest()`.
170        """
171
172        self.headers = {
173            "Content-Type": "application/json",
174            "accept": "application/json",
175            "Authorization": "Bearer {}".format(self.token),
176            "x-app-name": "Tim55667757.TKSBrokerAPI",
177        }
178        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
179
180        See also: `SendAPIRequest()`.
181        """
182
183        self.body = None
184        """Request body which send to broker server. Default: `None`.
185
186        See also: `SendAPIRequest()`.
187        """
188
189        self.moreDebug = False
190        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
191
192        self.historyFile = None
193        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
194
195        See also: `History()`.
196        """
197
198        self.htmlHistoryFile = "index.html"
199        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
200
201        See also: `ShowHistoryChart()`.
202        """
203
204        self.instrumentsFile = "instruments.md"
205        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
206
207        See also: `ShowInstrumentsInfo()`.
208        """
209
210        self.searchResultsFile = "search-results.md"
211        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
212
213        See also: `SearchInstruments()`.
214        """
215
216        self.pricesFile = "prices.md"
217        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
218
219        See also: `GetListOfPrices()`.
220        """
221
222        self.infoFile = "info.md"
223        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
224
225        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
226        """
227
228        self.bondsXLSXFile = "ext-bonds.xlsx"
229        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
230        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
231
232        See also: `ExtendBondsData()`.
233        """
234
235        self.calendarFile = "calendar.md"
236        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
237        
238        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
239
240        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
241        """
242
243        self.overviewFile = "overview.md"
244        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
245
246        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
247        """
248
249        self.overviewDigestFile = "overview-digest.md"
250        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
251
252        See also: `Overview()` with parameter `details="digest"`.
253        """
254
255        self.overviewPositionsFile = "overview-positions.md"
256        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
257
258        See also: `Overview()` with parameter `details="positions"`.
259        """
260
261        self.overviewOrdersFile = "overview-orders.md"
262        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
263
264        See also: `Overview()` with parameter `details="orders"`.
265        """
266
267        self.overviewAnalyticsFile = "overview-analytics.md"
268        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
269
270        See also: `Overview()` with parameter `details="analytics"`.
271        """
272
273        self.overviewBondsCalendarFile = "overview-calendar.md"
274        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
275
276        See also: `Overview()` with parameter `details="calendar"`.
277        """
278
279        self.reportFile = "deals.md"
280        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
281
282        See also: `Deals()`.
283        """
284
285        self.withdrawalLimitsFile = "limits.md"
286        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
287
288        See also: `OverviewLimits()` and `RequestLimits()`.
289        """
290
291        self.userInfoFile = "user-info.md"
292        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
293
294        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
295        """
296
297        self.userAccountsFile = "accounts.md"
298        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
299
300        See also: `OverviewAccounts()`, `RequestAccounts()`.
301        """
302
303        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
304        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
305
306        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
307
308        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
309        """
310
311        self.iList = None  # init iList for raw instruments data
312        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
313        
314        See also: `Listing()`, `DumpInstruments()`.
315        """
316
317        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
318        if useCache:
319            if os.path.exists(self.iListDumpFile):
320                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
321                curTime = datetime.now(tzutc())
322
323                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
324                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
325
326                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
327
328                else:
329                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
330
331                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
332                        os.path.abspath(self.iListDumpFile),
333                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
334                    ))
335
336            else:
337                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
338                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
339
340        else:
341            self.iList = self.Listing()  # request new raw instruments data from broker server
342            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
343
344        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
345        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
346
347        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
348        """

Main class init.

Parameters
  • token: Bearer token for Tinkoff Invest API. It can be set from environment variable TKS_API_TOKEN.
  • accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. Also, this variable can be set from environment variable TKS_ACCOUNT_ID.
  • useCache: use default cache file with raw data to use instead of iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then set useCache=False.
  • defaultCache: path to default cache file. dump.json by default.
version

Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.

Latest version: https://pypi.org/project/tksbrokerapi/

aliases

Some aliases instead official tickers.

See also: TKSEnums.TKS_TICKER_ALIASES

ticker

String with ticker, e.g. GOOGL. Tickers may be upper case only.

Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc. More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.

See also: SearchByTicker(), SearchInstruments().

figi

String with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6. FIGIs may be upper case only.

See also: SearchByFIGI(), SearchInstruments().

depth

Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.

See also: GetCurrentPrices().

server

Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest

See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().

timeout

Server operations timeout in seconds. Default: 15.

See also: SendAPIRequest().

headers

Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.

See also: SendAPIRequest().

body

Request body which send to broker server. Default: None.

See also: SendAPIRequest().

moreDebug

Enables more debug information in this class, such as net request and response headers in all methods. False by default.

historyFile

Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.

See also: History().

htmlHistoryFile

Full path to the html file where rendered candles chart stored. Default: index.html.

See also: ShowHistoryChart().

instrumentsFile

Filename where full available to user instruments list will be saved. Default: instruments.md.

See also: ShowInstrumentsInfo().

searchResultsFile

Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.

See also: SearchInstruments().

pricesFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: GetListOfPrices().

infoFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().

bondsXLSXFile

Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.

See also: ExtendBondsData().

calendarFile

Filename where bonds payment calendar will be saved. Default: calendar.md.

Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.

See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().

overviewFile

Filename where current portfolio, open trades and orders will be saved. Default: overview.md.

See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().

overviewDigestFile

Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.

See also: Overview() with parameter details="digest".

overviewPositionsFile

Filename where only open positions, without everything else will be saved. Default: overview-positions.md.

See also: Overview() with parameter details="positions".

overviewOrdersFile

Filename where open limits and stop orders will be saved. Default: overview-orders.md.

See also: Overview() with parameter details="orders".

overviewAnalyticsFile

Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.

See also: Overview() with parameter details="analytics".

overviewBondsCalendarFile

Filename where only the bonds calendar section will be saved. Default: overview-calendar.md.

See also: Overview() with parameter details="calendar".

reportFile

Filename where history of deals and trade statistics will be saved. Default: deals.md.

See also: Deals().

withdrawalLimitsFile

Filename where table of funds available for withdrawal will be saved. Default: limits.md.

See also: OverviewLimits() and RequestLimits().

userInfoFile

Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.

See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().

userAccountsFile

Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.

See also: OverviewAccounts(), RequestAccounts().

iListDumpFile

Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.

Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.

See also: DumpInstruments() and DumpInstrumentsAsXLSX().

iList

Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.

See also: Listing(), DumpInstruments().

priceModel

PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.

See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator

def SendAPIRequest( self, url: str, reqType: str = 'GET', retry: int = 3, pause: int = 5) -> dict:
364    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
365        """
366        Send GET or POST request to broker server and receive JSON object.
367
368        self.header: must be defining with dictionary of headers.
369        self.body: if define then used as request body. None by default.
370        self.timeout: global request timeout, 15 seconds by default.
371        :param url: url with REST request.
372        :param reqType: send "GET" or "POST" request. "GET" by default.
373        :param retry: how many times retry after first request if an 5xx server errors occurred.
374        :param pause: sleep time in seconds between retries.
375        :return: response JSON (dictionary) from broker.
376        """
377        if reqType not in ("GET", "POST"):
378            uLogger.error("You can define request type: 'GET' or 'POST'!")
379            raise Exception("Incorrect value")
380
381        if self.moreDebug:
382            uLogger.debug("Request parameters:")
383            uLogger.debug("    - REST API URL: {}".format(url))
384            uLogger.debug("    - request type: {}".format(reqType))
385            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
386            uLogger.debug("    - body:\n{}".format(self.body))
387
388        # fast hack to avoid all operations with some tickers/FIGI
389        responseJSON = {}
390        oK = True
391        for item in self.exclude:
392            if item in url:
393                if self.moreDebug:
394                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
395
396                oK = False
397                break
398
399        if oK:
400            counter = 0
401            response = None
402            errMsg = ""
403
404            while not response and counter <= retry:
405                if reqType == "GET":
406                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
407
408                if reqType == "POST":
409                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
410
411                if self.moreDebug:
412                    uLogger.debug("Response:")
413                    uLogger.debug("    - status code: {}".format(response.status_code))
414                    uLogger.debug("    - reason: {}".format(response.reason))
415                    uLogger.debug("    - body length: {}".format(len(response.text)))
416                    uLogger.debug("    - headers:\n{}".format(response.headers))
417
418                # Server returns some headers:
419                # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
420                # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
421                # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
422                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
423                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
424                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
425                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
426                    sleep(rateLimitWait)
427
428                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
429                if 400 <= response.status_code < 500:
430                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
431                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
432                    counter = retry + 1
433
434                if 500 <= response.status_code < 600:
435                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
436                    uLogger.debug("    - not oK, {}".format(errMsg))
437                    counter += 1
438
439                    if counter <= retry:
440                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
441                        sleep(pause)
442
443            responseJSON = self._ParseJSON(rawData=response.text)
444
445            if errMsg:
446                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
447                uLogger.error("    - not oK, {}".format(errMsg))
448
449        return responseJSON

Send GET or POST request to broker server and receive JSON object.

self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.

Parameters
  • url: url with REST request.
  • reqType: send "GET" or "POST" request. "GET" by default.
  • retry: how many times retry after first request if an 5xx server errors occurred.
  • pause: sleep time in seconds between retries.
Returns

response JSON (dictionary) from broker.

def Listing(self) -> dict:
482    def Listing(self) -> dict:
483        """
484        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
485
486        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
487        """
488        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
489        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
490
491        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
492        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
493        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
494
495        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
496        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
497        poolUpdater.close()
498
499        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
500        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
501        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
502
503        # calculate minimum price increment (step) for all instruments and set up instrument's type:
504        for iType in iList.keys():
505            for ticker in iList[iType]:
506                iList[iType][ticker]["type"] = iType
507
508                if "minPriceIncrement" in iList[iType][ticker].keys():
509                    iList[iType][ticker]["step"] = NanoToFloat(
510                        iList[iType][ticker]["minPriceIncrement"]["units"],
511                        iList[iType][ticker]["minPriceIncrement"]["nano"],
512                    )
513
514                else:
515                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
516
517        return iList

Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.

Returns

Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.

def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
519    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
520        """
521        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
522
523        See also: `DumpInstruments()`, `Listing()`.
524
525        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
526                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
527        """
528        if self.iListDumpFile is None or not self.iListDumpFile:
529            uLogger.error("Output name of dump file must be defined!")
530            raise Exception("Filename required")
531
532        if not self.iList or forceUpdate:
533            self.iList = self.Listing()
534
535        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
536
537        # Save as XLSX with separated sheets for every type of instruments:
538        with pd.ExcelWriter(
539                path=xlsxDumpFile,
540                date_format=TKS_DATE_FORMAT,
541                datetime_format=TKS_DATE_TIME_FORMAT,
542                mode="w",
543        ) as writer:
544            for iType in TKS_INSTRUMENTS:
545                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
546                df = df[sorted(df)]  # sorted by column names
547                df = df.applymap(
548                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
549                    na_action="ignore",
550                )  # converting numbers from nano-type to float in every cell
551                df.to_excel(
552                    writer,
553                    sheet_name=iType,
554                    encoding="UTF-8",
555                    freeze_panes=(1, 1),
556                )  # saving as XLSX-file with freeze first row and column as headers
557
558        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))

Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.

See also: DumpInstruments(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as XLSX-file (default: dump.xlsx) .
def DumpInstruments(self, forceUpdate: bool = True) -> str:
560    def DumpInstruments(self, forceUpdate: bool = True) -> str:
561        """
562        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
563        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
564
565        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
566
567        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
568                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
569        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
570        """
571        if self.iListDumpFile is None or not self.iListDumpFile:
572            uLogger.error("Output name of dump file must be defined!")
573            raise Exception("Filename required")
574
575        if not self.iList or forceUpdate:
576            self.iList = self.Listing()
577
578        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
579        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
580            fH.write(jsonDump)
581
582        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
583
584        return jsonDump

Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server using Listing() method. If iListDumpFile string is not empty then also save information to this file.

See also: DumpInstrumentsAsXLSX(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as JSON-file (default: dump.json).
Returns

serialized JSON formatted str with full data of instruments, also saved to the --output JSON-file.

def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
586    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
587        """
588        Show information about one instrument defined by json data and prints it in Markdown format.
589
590        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
591
592        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]`
593        :param show: if `True` then also printing information about instrument and its current price.
594        :return: multilines text in Markdown format with information about one instrument.
595        """
596        splitLine = "|                                                             |                                                        |\n"
597        infoText = ""
598
599        if iJSON is not None and iJSON and isinstance(iJSON, dict):
600            info = [
601                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
602                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
603                "| Parameters                                                  | Values                                                 |\n",
604                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
605                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
606                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
607            ]
608
609            if "sector" in iJSON.keys() and iJSON["sector"]:
610                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
611
612            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
613                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
614
615            info.extend([
616                splitLine,
617                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
618                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
619            ])
620
621            if "isin" in iJSON.keys() and iJSON["isin"]:
622                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
623
624            if "classCode" in iJSON.keys():
625                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
626
627            info.extend([
628                splitLine,
629                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
630                splitLine,
631                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
632                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
633                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
634            ])
635
636            if iJSON["figi"]:
637                self.figi = iJSON["figi"]
638                iJSON = iJSON | self.RequestTradingStatus()
639
640                info.extend([
641                    splitLine,
642                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
643                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
644                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
645                ])
646
647            info.append(splitLine)
648
649            if "type" in iJSON.keys() and iJSON["type"]:
650                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
651
652                if "shareType" in iJSON.keys() and iJSON["shareType"]:
653                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
654
655            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
656                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
657
658            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
659                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
660
661            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
662                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
663
664            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
665                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
666
667            if "focusType" in iJSON.keys() and iJSON["focusType"]:
668                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
669
670            if "assetType" in iJSON.keys() and iJSON["assetType"]:
671                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
672
673            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
674                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
675
676            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
677                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
678
679            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
680                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
681
682            if "currency" in iJSON.keys():
683                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
684
685            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
686                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
687
688            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
689                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
690
691            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
692                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
693
694            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
695                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
696
697            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
698                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
699
700            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
701                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
702
703            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
704                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
705
706            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
707                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
708
709            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
710                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
711
712            iExt = None
713            if iJSON["type"] == "Bonds":
714                info.extend([
715                    splitLine,
716                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
717                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
718                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
719                        iJSON["nominal"]["currency"],
720                    )),
721                ])
722
723                if "floatingCouponFlag" in iJSON.keys():
724                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
725
726                if "amortizationFlag" in iJSON.keys():
727                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
728
729                info.append(splitLine)
730
731                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
732                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
733
734                if iJSON["figi"]:
735                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
736
737                    info.extend([
738                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
739                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
740                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
741                    ])
742
743                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
744                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
745                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
746                        iJSON["aciValue"]["currency"]
747                    )))
748
749            if "currentPrice" in iJSON.keys():
750                info.append(splitLine)
751
752                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
753                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
754
755                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
756                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
757                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
758                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
759                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
760
761                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
762                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
763
764                info.extend([
765                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
766                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
767                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
768                    )),
769                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
770                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
771                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
772                    )),
773                    "| Changes between last deal price and last close              | {:<54} |\n".format(
774                        "{:.2f}%{}".format(
775                            iJSON["currentPrice"]["changes"],
776                            " ({}{:.2f} {})".format(
777                                "+" if bondChangesDelta > 0 else "",
778                                bondChangesDelta,
779                                aciCurrency
780                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
781                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
782                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
783                                currency
784                            ),
785                        )
786                    ),
787                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
788                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
789                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
790                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
791                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
792                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
793                    )),
794                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
795                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
796                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
797                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
798                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
799                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
800                    )),
801                ])
802
803            if "lot" in iJSON.keys():
804                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
805
806            if "step" in iJSON.keys() and iJSON["step"] != 0:
807                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
808
809            # Add bond payment calendar:
810            if iJSON["type"] == "Bonds":
811                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
812                info.extend(["\n", strCalendar])
813
814            infoText += "".join(info)
815
816            if show:
817                uLogger.info("{}".format(infoText))
818
819            else:
820                uLogger.debug("{}".format(infoText))
821
822            if self.infoFile is not None:
823                with open(self.infoFile, "w", encoding="UTF-8") as fH:
824                    fH.write(infoText)
825
826                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
827
828        return infoText

Show information about one instrument defined by json data and prints it in Markdown format.

See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().

Parameters
  • iJSON: json data of instrument, example: iJSON = self.iList["Shares"][self.ticker]
  • show: if True then also printing information about instrument and its current price.
Returns

multilines text in Markdown format with information about one instrument.

def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
830    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
831        """
832        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
833
834        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
835        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
836        :return: JSON formatted data with information about instrument.
837        """
838        tickerJSON = {}
839        if self.moreDebug:
840            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker))
841
842        if not self.ticker:
843            uLogger.warning("self.ticker variable is not be empty!")
844
845        else:
846            if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
847                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker))
848                raise Exception("Instrument not allowed")
849
850            if not self.iList:
851                self.iList = self.Listing()
852
853            if self.ticker in self.iList["Shares"].keys():
854                tickerJSON = self.iList["Shares"][self.ticker]
855                if self.moreDebug:
856                    uLogger.debug("Ticker [{}] found in shares list".format(self.ticker))
857
858            elif self.ticker in self.iList["Currencies"].keys():
859                tickerJSON = self.iList["Currencies"][self.ticker]
860                if self.moreDebug:
861                    uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker))
862
863            elif self.ticker in self.iList["Bonds"].keys():
864                tickerJSON = self.iList["Bonds"][self.ticker]
865                if self.moreDebug:
866                    uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker))
867
868            elif self.ticker in self.iList["Etfs"].keys():
869                tickerJSON = self.iList["Etfs"][self.ticker]
870                if self.moreDebug:
871                    uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker))
872
873            elif self.ticker in self.iList["Futures"].keys():
874                tickerJSON = self.iList["Futures"][self.ticker]
875                if self.moreDebug:
876                    uLogger.debug("Ticker [{}] found in futures list".format(self.ticker))
877
878        if tickerJSON:
879            self.figi = tickerJSON["figi"]
880
881            if requestPrice:
882                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
883
884                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
885                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
886
887                else:
888                    tickerJSON["currentPrice"]["changes"] = 0
889
890            if show:
891                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
892
893        else:
894            if show:
895                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker))
896
897        return tickerJSON

Search and return raw broker's information about instrument by its ticker. Variable ticker must be defined!

Parameters
  • requestPrice: if False then do not request current price of instrument (because this is long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
Returns

JSON formatted data with information about instrument.

def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
899    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
900        """
901        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
902
903        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
904        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
905        :return: JSON formatted data with information about instrument.
906        """
907        figiJSON = {}
908        if self.moreDebug:
909            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi))
910
911        if not self.figi:
912            uLogger.warning("self.figi variable is not be empty!")
913
914        else:
915            if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
916                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi))
917                raise Exception("Instrument not allowed")
918
919            if not self.iList:
920                self.iList = self.Listing()
921
922            for item in self.iList["Shares"].keys():
923                if self.figi == self.iList["Shares"][item]["figi"]:
924                    figiJSON = self.iList["Shares"][item]
925
926                    if self.moreDebug:
927                        uLogger.debug("FIGI [{}] found in shares list".format(self.figi))
928
929                    break
930
931            if not figiJSON:
932                for item in self.iList["Currencies"].keys():
933                    if self.figi == self.iList["Currencies"][item]["figi"]:
934                        figiJSON = self.iList["Currencies"][item]
935
936                        if self.moreDebug:
937                            uLogger.debug("FIGI [{}] found in currencies list".format(self.figi))
938
939                        break
940
941            if not figiJSON:
942                for item in self.iList["Bonds"].keys():
943                    if self.figi == self.iList["Bonds"][item]["figi"]:
944                        figiJSON = self.iList["Bonds"][item]
945
946                        if self.moreDebug:
947                            uLogger.debug("FIGI [{}] found in bonds list".format(self.figi))
948
949                        break
950
951            if not figiJSON:
952                for item in self.iList["Etfs"].keys():
953                    if self.figi == self.iList["Etfs"][item]["figi"]:
954                        figiJSON = self.iList["Etfs"][item]
955
956                        if self.moreDebug:
957                            uLogger.debug("FIGI [{}] found in etfs list".format(self.figi))
958
959                        break
960
961            if not figiJSON:
962                for item in self.iList["Futures"].keys():
963                    if self.figi == self.iList["Futures"][item]["figi"]:
964                        figiJSON = self.iList["Futures"][item]
965
966                        if self.moreDebug:
967                            uLogger.debug("FIGI [{}] found in futures list".format(self.figi))
968
969                        break
970
971        if figiJSON:
972            self.figi = figiJSON["figi"]
973            self.ticker = figiJSON["ticker"]
974
975            if requestPrice:
976                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
977
978                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
979                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
980
981                else:
982                    figiJSON["currentPrice"]["changes"] = 0
983
984            if show:
985                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
986
987        else:
988            if show:
989                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi))
990
991        return figiJSON

Search and return raw broker's information about instrument by its FIGI. Variable figi must be defined!

Parameters
  • requestPrice: if False then do not request current price of instrument (it's long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
Returns

JSON formatted data with information about instrument.

def GetCurrentPrices(self, show: bool = True) -> dict:
 993    def GetCurrentPrices(self, show: bool = True) -> dict:
 994        """
 995        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
 996        `{"buy": [{"price": 1243.8, "quantity": 193},
 997                  {"price": 1244.0, "quantity": 168},
 998                  {"price": 1244.8, "quantity": 5},
 999                  {"price": 1245.0, "quantity": 61},
1000                  {"price": 1245.4, "quantity": 60}],
1001          "sell": [{"price": 1243.6, "quantity": 8},
1002                   {"price": 1242.6, "quantity": 10},
1003                   {"price": 1242.4, "quantity": 18},
1004                   {"price": 1242.2, "quantity": 50},
1005                   {"price": 1242.0, "quantity": 113}],
1006          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1007        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1008        - sell: list of dicts with Buyers prices,
1009            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1010            - quantity: volume value by current price in lots,
1011        - limitUp: current trade session limit price, maximum,
1012        - limitDown: current trade session limit price, minimum,
1013        - lastPrice: last deal price of the instrument,
1014        - closePrice: previous trade session close price of the instrument.
1015
1016        See also: `SearchByTicker()` and `SearchByFIGI()`.
1017        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1018        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1019
1020        :param show: if `True` then print DOM to log and console.
1021        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1022                 If an error occurred then returns an empty record:
1023                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1024        """
1025        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1026
1027        if self.depth < 1:
1028            uLogger.error("Depth of Market (DOM) must be >=1!")
1029            raise Exception("Incorrect value")
1030
1031        if not (self.ticker or self.figi):
1032            uLogger.error("self.ticker or self.figi variables must be defined!")
1033            raise Exception("Ticker or FIGI required")
1034
1035        if self.ticker and not self.figi:
1036            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1037            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1038
1039        if not self.ticker and self.figi:
1040            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1041            self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1042
1043        if not self.figi:
1044            uLogger.error("FIGI is not defined!")
1045            raise Exception("Ticker or FIGI required")
1046
1047        else:
1048            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi))
1049
1050            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1051            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1052            self.body = str({"figi": self.figi, "depth": self.depth})
1053            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1054
1055            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1056                # list of dicts with sellers orders:
1057                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1058
1059                # list of dicts with buyers orders:
1060                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1061
1062                # max price of instrument at this time:
1063                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1064
1065                # min price of instrument at this time:
1066                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1067
1068                # last price of deal with instrument:
1069                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1070
1071                # last close price of instrument:
1072                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1073
1074            else:
1075                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1076                uLogger.debug("Server response: {}".format(pricesResponse))
1077
1078            if show:
1079                if prices["buy"] or prices["sell"]:
1080                    info = [
1081                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1082                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1083                            self.ticker,
1084                            self.figi,
1085                            self.depth,
1086                        ),
1087                        "-" * 60, "\n",
1088                        "             Orders of Buyers | Orders of Sellers\n",
1089                        "-" * 60, "\n",
1090                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1091                        "-" * 60, "\n",
1092                    ]
1093
1094                    if not prices["buy"]:
1095                        info.append("                              | No orders!\n")
1096                        sumBuy = 0
1097
1098                    else:
1099                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1100                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1101                        for item in maxMinSorted:
1102                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1103
1104                    if not prices["sell"]:
1105                        info.append("No orders!                    |\n")
1106                        sumSell = 0
1107
1108                    else:
1109                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1110                        for item in prices["sell"]:
1111                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1112
1113                    info.extend([
1114                        "-" * 60, "\n",
1115                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1116                        "-" * 60, "\n",
1117                    ])
1118
1119                    infoText = "".join(info)
1120
1121                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1122
1123                else:
1124                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1125
1126        return prices

Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5: {"buy": [{"price": 1243.8, "quantity": 193}, {"price": 1244.0, "quantity": 168}, {"price": 1244.8, "quantity": 5}, {"price": 1245.0, "quantity": 61}, {"price": 1245.4, "quantity": 60}], "sell": [{"price": 1243.6, "quantity": 8}, {"price": 1242.6, "quantity": 10}, {"price": 1242.4, "quantity": 18}, {"price": 1242.2, "quantity": 50}, {"price": 1242.0, "quantity": 113}], "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:

  • buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
  • sell: list of dicts with Buyers prices,
    • price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
    • quantity: volume value by current price in lots,
  • limitUp: current trade session limit price, maximum,
  • limitDown: current trade session limit price, minimum,
  • lastPrice: last deal price of the instrument,
  • closePrice: previous trade session close price of the instrument.

See also: SearchByTicker() and SearchByFIGI(). REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse

Parameters
  • show: if True then print DOM to log and console.
Returns

orders book dict with lists of current buy and sell prices: {"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record: {"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.

def ShowInstrumentsInfo(self, show: bool = True) -> str:
1128    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1129        """
1130        This method get and show information about all available broker instruments for current user account.
1131        If `instrumentsFile` string is not empty then also save information to this file.
1132
1133        :param show: if `True` then print results to console, if `False` — print only to file.
1134        :return: multi-lines string with all available broker instruments
1135        """
1136        if not self.iList:
1137            self.iList = self.Listing()
1138
1139        info = [
1140            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1141            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1142        ]
1143
1144        # add instruments count by type:
1145        for iType in self.iList.keys():
1146            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1147
1148        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1149        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1150
1151        # generating info tables with all instruments by type:
1152        for iType in self.iList.keys():
1153            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1154
1155            for instrument in self.iList[iType].keys():
1156                iName = self.iList[iType][instrument]["name"]  # instrument's name
1157                if len(iName) > 57:
1158                    iName = "{}...".format(iName[:54])  # right trim for a long string
1159
1160                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1161                    self.iList[iType][instrument]["ticker"],
1162                    iName,
1163                    self.iList[iType][instrument]["figi"],
1164                    self.iList[iType][instrument]["currency"],
1165                    self.iList[iType][instrument]["lot"],
1166                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1167                ))
1168
1169        infoText = "".join(info)
1170
1171        if show:
1172            uLogger.info(infoText)
1173
1174        if self.instrumentsFile:
1175            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1176                fH.write(infoText)
1177
1178            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1179
1180        return infoText

This method get and show information about all available broker instruments for current user account. If instrumentsFile string is not empty then also save information to this file.

Parameters
  • show: if True then print results to console, if False — print only to file.
Returns

multi-lines string with all available broker instruments

def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1182    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1183        """
1184        This method search and show information about instruments by part of its ticker, FIGI or name.
1185        If `searchResultsFile` string is not empty then also save information to this file.
1186
1187        :param pattern: string with part of ticker, FIGI or instrument's name.
1188        :param show: if `True` then print results to console, if `False` — return list of result only.
1189        :return: list of dictionaries with all found instruments.
1190        """
1191        if not self.iList:
1192            self.iList = self.Listing()
1193
1194        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1195        compiledPattern = re.compile(pattern, re.IGNORECASE)
1196
1197        for iType in self.iList:
1198            for instrument in self.iList[iType].values():
1199                searchResult = compiledPattern.search(" ".join(
1200                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1201                ))
1202
1203                if searchResult:
1204                    searchResults[iType][instrument["ticker"]] = instrument
1205
1206        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1207        info = [
1208            "# Search results\n\n",
1209            "* **Search pattern:** [{}]\n".format(pattern),
1210            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1211            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1212        ]
1213        infoShort = info[:]
1214
1215        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1216        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1217        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1218
1219        if resultsLen == 0:
1220            info.append("\nNo results\n")
1221            infoShort.append("\nNo results\n")
1222            uLogger.warning("No results. Try changing your search pattern.")
1223
1224        else:
1225            for iType in searchResults:
1226                iTypeValuesCount = len(searchResults[iType].values())
1227                if iTypeValuesCount > 0:
1228                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1229                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1230
1231                    for instrument in searchResults[iType].values():
1232                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1233                            instrument["type"],
1234                            instrument["ticker"],
1235                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1236                            instrument["figi"],
1237                        ))
1238
1239                    if iTypeValuesCount <= 5:
1240                        infoShort.extend(info[-iTypeValuesCount:])
1241
1242                    else:
1243                        infoShort.extend(info[-5:])
1244                        infoShort.append(skippedLine)
1245
1246        infoText = "".join(info)
1247        infoTextShort = "".join(infoShort)
1248
1249        if show:
1250            uLogger.info(infoTextShort)
1251            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1252
1253        if self.searchResultsFile:
1254            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1255                fH.write(infoText)
1256
1257            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1258
1259        return searchResults

This method search and show information about instruments by part of its ticker, FIGI or name. If searchResultsFile string is not empty then also save information to this file.

Parameters
  • pattern: string with part of ticker, FIGI or instrument's name.
  • show: if True then print results to console, if False — return list of result only.
Returns

list of dictionaries with all found instruments.

def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1261    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1262        """
1263        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1264
1265        :param instruments: list of strings with tickers or FIGIs.
1266        :return: list with unique instrument FIGIs only.
1267        """
1268        requestedInstruments = []
1269        for iName in instruments:
1270            if iName not in self.aliases.keys():
1271                if iName not in requestedInstruments:
1272                    requestedInstruments.append(iName)
1273
1274            else:
1275                if iName not in requestedInstruments:
1276                    if self.aliases[iName] not in requestedInstruments:
1277                        requestedInstruments.append(self.aliases[iName])
1278
1279        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1280
1281        onlyUniqueFIGIs = []
1282        for iName in requestedInstruments:
1283            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1284                continue
1285
1286            self.ticker = iName
1287            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1288
1289            if not iData:
1290                self.ticker = ""
1291                self.figi = iName
1292
1293                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1294
1295                if not iData:
1296                    self.figi = ""
1297                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1298
1299            if iData and iData["figi"] not in onlyUniqueFIGIs:
1300                onlyUniqueFIGIs.append(iData["figi"])
1301
1302        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1303
1304        return onlyUniqueFIGIs

Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.

Parameters
  • instruments: list of strings with tickers or FIGIs.
Returns

list with unique instrument FIGIs only.

def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1306    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1307        """
1308        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1309
1310        See limits: https://tinkoff.github.io/investAPI/limits/
1311
1312        If `pricesFile` string is not empty then also save information to this file.
1313
1314        :param instruments: list of strings with tickers or FIGIs.
1315        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1316        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1317                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1318        """
1319        if instruments is None or not instruments:
1320            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1321            raise Exception("Ticker or FIGI required")
1322
1323        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1324
1325        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1326
1327        iList = []  # trying to get info and current prices about all unique instruments:
1328        for self.figi in onlyUniqueFIGIs:
1329            iData = self.SearchByFIGI(requestPrice=True)
1330            iList.append(iData)
1331
1332        self.ShowListOfPrices(iList, show)
1333
1334        return iList

This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!

See limits: https://tinkoff.github.io/investAPI/limits/

If pricesFile string is not empty then also save information to this file.

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • show: if True then prints prices to console, if False — prints only to file pricesFile.
Returns

list of instruments looks like [{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker() or SearchByFIGI() methods.

def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1336    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1337        """
1338        Show table contains current prices of given instruments.
1339
1340        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1341                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1342        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1343        :return: multilines text in Markdown format as a table contains current prices.
1344        """
1345        infoText = ""
1346
1347        if show or self.pricesFile:
1348            info = [
1349                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1350                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1351                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1352            ]
1353
1354            for item in iList:
1355                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1356                    item["ticker"],
1357                    item["figi"],
1358                    item["type"],
1359                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1360                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1361                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1362                    "{} / {}".format(
1363                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1364                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1365                    ),
1366                    "{} / {}".format(
1367                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1368                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1369                    ),
1370                    item["currency"],
1371                ))
1372
1373            infoText = "".join(info)
1374
1375            if show:
1376                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1377
1378            if self.pricesFile:
1379                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1380                    fH.write(infoText)
1381
1382                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1383
1384        return infoText

Show table contains current prices of given instruments.

Parameters
  • **iList: list of instruments looks like [{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker(requestPrice=True) or by SearchByFIGI(requestPrice=True) methods.
  • show: if True then prints prices to console, if False — prints only to file pricesFile.
Returns

multilines text in Markdown format as a table contains current prices.

def RequestTradingStatus(self) -> dict:
1386    def RequestTradingStatus(self) -> dict:
1387        """
1388        Requesting trading status for the instrument defined by `figi` variable.
1389
1390        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1391
1392        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1393
1394        :return: dictionary with trading status attributes. Response example:
1395                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1396                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1397        """
1398        if self.figi is None or not self.figi:
1399            uLogger.error("Variable `figi` must be defined for using this method!")
1400            raise Exception("FIGI required")
1401
1402        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi))
1403
1404        self.body = str({"figi": self.figi, "instrumentId": self.figi})
1405        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1406        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1407
1408        if self.moreDebug:
1409            uLogger.debug("Records about current trading status successfully received")
1410
1411        return tradingStatus

Requesting trading status for the instrument defined by figi variable.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus

Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest

Returns

dictionary with trading status attributes. Response example: {"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}

def RequestPortfolio(self) -> dict:
1413    def RequestPortfolio(self) -> dict:
1414        """
1415        Requesting actual user's portfolio for current `accountId`.
1416
1417        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1418
1419        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1420
1421        :return: dictionary with user's portfolio.
1422        """
1423        if self.accountId is None or not self.accountId:
1424            uLogger.error("Variable `accountId` must be defined for using this method!")
1425            raise Exception("Account ID required")
1426
1427        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1428
1429        self.body = str({"accountId": self.accountId})
1430        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1431        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1432
1433        if self.moreDebug:
1434            uLogger.debug("Records about user's portfolio successfully received")
1435
1436        return rawPortfolio

Requesting actual user's portfolio for current accountId.

REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio

Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest

Returns

dictionary with user's portfolio.

def RequestPositions(self) -> dict:
1438    def RequestPositions(self) -> dict:
1439        """
1440        Requesting open positions by currencies and instruments for current `accountId`.
1441
1442        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1443
1444        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1445
1446        :return: dictionary with open positions by instruments.
1447        """
1448        if self.accountId is None or not self.accountId:
1449            uLogger.error("Variable `accountId` must be defined for using this method!")
1450            raise Exception("Account ID required")
1451
1452        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1453
1454        self.body = str({"accountId": self.accountId})
1455        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1456        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1457
1458        if self.moreDebug:
1459            uLogger.debug("Records about current open positions successfully received")
1460
1461        return rawPositions

Requesting open positions by currencies and instruments for current accountId.

REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions

Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest

Returns

dictionary with open positions by instruments.

def RequestPendingOrders(self) -> list:
1463    def RequestPendingOrders(self) -> list:
1464        """
1465        Requesting current actual pending orders for current `accountId`.
1466
1467        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1468
1469        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1470
1471        :return: list of dictionaries with pending orders.
1472        """
1473        if self.accountId is None or not self.accountId:
1474            uLogger.error("Variable `accountId` must be defined for using this method!")
1475            raise Exception("Account ID required")
1476
1477        uLogger.debug("Requesting current actual pending orders. Wait, please...")
1478
1479        self.body = str({"accountId": self.accountId})
1480        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1481        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1482
1483        uLogger.debug("[{}] records about pending orders received".format(len(rawOrders)))
1484
1485        return rawOrders

Requesting current actual pending orders for current accountId.

REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders

Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest

Returns

list of dictionaries with pending orders.

def RequestStopOrders(self) -> list:
1487    def RequestStopOrders(self) -> list:
1488        """
1489        Requesting current actual stop orders for current `accountId`.
1490
1491        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1492
1493        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1494
1495        :return: list of dictionaries with stop orders.
1496        """
1497        if self.accountId is None or not self.accountId:
1498            uLogger.error("Variable `accountId` must be defined for using this method!")
1499            raise Exception("Account ID required")
1500
1501        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1502
1503        self.body = str({"accountId": self.accountId})
1504        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1505        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1506
1507        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1508
1509        return rawStopOrders

Requesting current actual stop orders for current accountId.

REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders

Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest

Returns

list of dictionaries with stop orders.

def Overview(self, show: bool = False, details: str = 'full') -> dict:
1511    def Overview(self, show: bool = False, details: str = "full") -> dict:
1512        """
1513        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1514        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1515        and `overviewBondsCalendarFile` are defined then also save information to file.
1516
1517        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1518        many requests about the state of the portfolio, and then, based on the received data, a large number
1519        of calculation and statistics are collected.
1520
1521        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1522        :param details: how detailed should the information be?
1523        - `full` — shows full available information about portfolio status (by default),
1524        - `positions` — shows only open positions,
1525        - `orders` — shows only sections of open limits and stop orders.
1526        - `digest` — show a short digest of the portfolio status,
1527        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1528        - `calendar` — shows only the bonds calendar section (if these present in portfolio),
1529        :return: dictionary with client's raw portfolio and some statistics.
1530        """
1531        if self.accountId is None or not self.accountId:
1532            uLogger.error("Variable `accountId` must be defined for using this method!")
1533            raise Exception("Account ID required")
1534
1535        view = {
1536            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1537                "headers": {},  # list of dictionaries, response headers without "positions" section
1538                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1539                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1540                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1541                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1542                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1543                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1544                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1545                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1546                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1547            },
1548            "stat": {  # --- some statistics calculated using "raw" sections:
1549                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1550                "availableRUB": 0.,  # available rubles (without other currencies)
1551                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1552                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1553                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1554                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1555                "sharesCostRUB": 0.,  # costs of all shares in RUB
1556                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1557                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1558                "futuresCostRUB": 0.,  # costs of all futures in RUB
1559                "Currencies": [],  # list of dictionaries of all currencies statistics
1560                "Shares": [],  # list of dictionaries of all shares statistics
1561                "Bonds": [],  # list of dictionaries of all bonds statistics
1562                "Etfs": [],  # list of dictionaries of all etfs statistics
1563                "Futures": [],  # list of dictionaries of all futures statistics
1564                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1565                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1566                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1567                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1568                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1569            },
1570            "analytics": {  # --- some analytics of portfolio:
1571                "distrByAssets": {},  # portfolio distribution by assets
1572                "distrByCompanies": {},  # portfolio distribution by companies
1573                "distrBySectors": {},  # portfolio distribution by sectors
1574                "distrByCurrencies": {},  # portfolio distribution by currencies
1575                "distrByCountries": {},  # portfolio distribution by countries
1576                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1577            }
1578        }
1579
1580        details = details.lower()
1581        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1582        if details not in availableDetails:
1583            details = "full"
1584            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1585
1586        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1587
1588        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1589        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1590        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending orders (list)
1591        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1592
1593        # save response headers without "positions" section:
1594        for key in portfolioResponse.keys():
1595            if key != "positions":
1596                view["raw"]["headers"][key] = portfolioResponse[key]
1597
1598            else:
1599                continue
1600
1601        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1602        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1603        for item in portfolioResponse["positions"]:
1604            if item["instrumentType"] == "currency":
1605                self.figi = item["figi"]
1606                curr = self.SearchByFIGI(requestPrice=False)
1607
1608                # current price of currency in RUB:
1609                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1610                    "name": curr["name"],
1611                    "currentPrice": NanoToFloat(
1612                        item["currentPrice"]["units"],
1613                        item["currentPrice"]["nano"]
1614                    ),
1615                }
1616
1617                view["raw"]["Currencies"].append(item)
1618
1619            elif item["instrumentType"] == "share":
1620                view["raw"]["Shares"].append(item)
1621
1622            elif item["instrumentType"] == "bond":
1623                view["raw"]["Bonds"].append(item)
1624
1625            elif item["instrumentType"] == "etf":
1626                view["raw"]["Etfs"].append(item)
1627
1628            elif item["instrumentType"] == "futures":
1629                view["raw"]["Futures"].append(item)
1630
1631            else:
1632                continue
1633
1634        # how many volume of currencies (by ISO currency name) are blocked:
1635        for item in view["raw"]["positions"]["blocked"]:
1636            blocked = NanoToFloat(item["units"], item["nano"])
1637            if blocked > 0:
1638                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1639
1640        # how many volume of instruments (by FIGI) are blocked:
1641        for item in view["raw"]["positions"]["securities"]:
1642            blocked = int(item["blocked"])
1643            if blocked > 0:
1644                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1645
1646        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1647
1648        if "rub" in allBlocked.keys():
1649            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1650
1651        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1652        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1653        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1654        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1655        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1656        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1657        view["stat"]["portfolioCostRUB"] = sum([
1658            view["stat"]["allCurrenciesCostRUB"],
1659            view["stat"]["sharesCostRUB"],
1660            view["stat"]["bondsCostRUB"],
1661            view["stat"]["etfsCostRUB"],
1662            view["stat"]["futuresCostRUB"],
1663        ])
1664
1665        # --- calculating some portfolio statistics:
1666        byComp = {}  # distribution by companies
1667        bySect = {}  # distribution by sectors
1668        byCurr = {}  # distribution by currencies (include RUB)
1669        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1670        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1671
1672        for item in portfolioResponse["positions"]:
1673            self.figi = item["figi"]
1674            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1675
1676            if instrument:
1677                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1678                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1679
1680                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1681                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1682
1683                else:
1684                    blocked = 0
1685
1686                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1687                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1688                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1689                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1690                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1691                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1692                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1693                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1694                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1695                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1696                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1697                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1698
1699                statData = {
1700                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1701                    "ticker": instrument["ticker"],  # ticker by FIGI
1702                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1703                    "volume": volume,  # available volume of instrument
1704                    "lots": lots,  # volume in lots of instrument
1705                    "direction": direction,  # direction of an instrument's position: short or long
1706                    "blocked": blocked,  # blocked volume of currency or instrument
1707                    "currentPrice": curPrice,  # current instrument's price in basic asset
1708                    "average": average,  # current average position price
1709                    "cost": cost,  # current cost of all volume of instrument in basic asset
1710                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1711                    "costRUB": costRUB,  # cost of instrument in ruble
1712                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1713                    "profit": profit,  # expected profit at current moment
1714                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1715                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1716                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1717                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1718                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1719                    "step": instrument["step"],  # minimum price increment
1720                }
1721
1722                # adding distribution by unique countries:
1723                if statData["country"] not in byCountry.keys():
1724                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1725
1726                else:
1727                    byCountry[statData["country"]]["cost"] += costRUB
1728                    byCountry[statData["country"]]["percent"] += percentCostRUB
1729
1730                if item["instrumentType"] != "currency":
1731                    # adding distribution by unique companies:
1732                    if statData["name"]:
1733                        if statData["name"] not in byComp.keys():
1734                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1735
1736                        else:
1737                            byComp[statData["name"]]["cost"] += costRUB
1738                            byComp[statData["name"]]["percent"] += percentCostRUB
1739
1740                    # adding distribution by unique sectors:
1741                    if statData["sector"] not in bySect.keys():
1742                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1743
1744                    else:
1745                        bySect[statData["sector"]]["cost"] += costRUB
1746                        bySect[statData["sector"]]["percent"] += percentCostRUB
1747
1748                # adding distribution by unique currencies:
1749                if currency not in byCurr.keys():
1750                    byCurr[currency] = {
1751                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1752                        "cost": costRUB,
1753                        "percent": percentCostRUB
1754                    }
1755
1756                else:
1757                    byCurr[currency]["cost"] += costRUB
1758                    byCurr[currency]["percent"] += percentCostRUB
1759
1760                # saving statistics for every instrument:
1761                if item["instrumentType"] == "currency":
1762                    view["stat"]["Currencies"].append(statData)
1763
1764                    # update dict with free funds for trading (total - blocked) by currencies
1765                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1766                    view["stat"]["funds"][currency] = {
1767                        "total": volume,
1768                        "totalCostRUB": costRUB,  # total volume cost in rubles
1769                        "free": volume - blocked,
1770                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1771                    }
1772
1773                elif item["instrumentType"] == "share":
1774                    view["stat"]["Shares"].append(statData)
1775
1776                elif item["instrumentType"] == "bond":
1777                    view["stat"]["Bonds"].append(statData)
1778
1779                elif item["instrumentType"] == "etf":
1780                    view["stat"]["Etfs"].append(statData)
1781
1782                elif item["instrumentType"] == "Futures":
1783                    view["stat"]["Futures"].append(statData)
1784
1785                else:
1786                    continue
1787
1788        # total changes in Russian Ruble:
1789        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1790        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1791        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1792        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1793        view["stat"]["funds"]["rub"] = {
1794            "total": view["stat"]["availableRUB"],
1795            "totalCostRUB": view["stat"]["availableRUB"],
1796            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1797            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1798        }
1799
1800        # --- pending orders sector data:
1801        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending orders to avoid many times price requests
1802        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1803
1804        for item in view["raw"]["orders"]:
1805            self.figi = item["figi"]
1806
1807            if item["figi"] not in uniquePendingOrdersFIGIs:
1808                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1809
1810                uniquePendingOrdersFIGIs.append(item["figi"])
1811                uniquePendingOrders[item["figi"]] = instrument
1812
1813            else:
1814                instrument = uniquePendingOrders[item["figi"]]
1815
1816            if instrument:
1817                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1818                orderType = TKS_ORDER_TYPES[item["orderType"]]
1819                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1820                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1821
1822                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1823                if item["direction"] == "ORDER_DIRECTION_BUY":
1824                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1825
1826                else:
1827                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1828
1829                # requested price for order execution:
1830                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1831
1832                # necessary changes in percent to reach target from current price:
1833                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1834
1835                view["stat"]["orders"].append({
1836                    "orderID": item["orderId"],  # orderId number parameter of current order
1837                    "figi": item["figi"],  # FIGI identification
1838                    "ticker": instrument["ticker"],  # ticker name by FIGI
1839                    "lotsRequested": item["lotsRequested"],  # requested lots value
1840                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1841                    "currentPrice": lastPrice,  # current instrument's price for defined action
1842                    "targetPrice": target,  # requested price for order execution in base currency
1843                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1844                    "percentChanges": changes,  # changes in percent to target from current price
1845                    "currency": item["currency"],  # instrument's currency name
1846                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1847                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1848                    "status": orderState,  # order status from TKS_ORDER_STATES
1849                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1850                })
1851
1852        # --- stop orders sector data:
1853        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1854        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1855
1856        for item in view["raw"]["stopOrders"]:
1857            self.figi = item["figi"]
1858
1859            if item["figi"] not in uniqueStopOrdersFIGIs:
1860                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1861
1862                uniqueStopOrdersFIGIs.append(item["figi"])
1863                uniqueStopOrders[item["figi"]] = instrument
1864
1865            else:
1866                instrument = uniqueStopOrders[item["figi"]]
1867
1868            if instrument:
1869                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1870                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1871                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1872
1873                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1874                if "expirationTime" in item.keys():
1875                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1876                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1877
1878                else:
1879                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1880                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1881
1882                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1883                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1884                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1885
1886                else:
1887                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1888
1889                # requested price when stop-order executed:
1890                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1891
1892                # price for limit-order, set up when stop-order executed:
1893                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1894
1895                # necessary changes in percent to reach target from current price:
1896                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1897
1898                view["stat"]["stopOrders"].append({
1899                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
1900                    "figi": item["figi"],  # FIGI identification
1901                    "ticker": instrument["ticker"],  # ticker name by FIGI
1902                    "lotsRequested": item["lotsRequested"],  # requested lots value
1903                    "currentPrice": lastPrice,  # current instrument's price for defined action
1904                    "targetPrice": target,  # requested price for stop-order execution in base currency
1905                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
1906                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
1907                    "percentChanges": changes,  # changes in percent to target from current price
1908                    "currency": item["currency"],  # instrument's currency name
1909                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
1910                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
1911                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
1912                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
1913                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
1914                })
1915
1916        # --- calculating data for analytics section:
1917        # portfolio distribution by assets:
1918        view["analytics"]["distrByAssets"] = {
1919            "Ruble": {
1920                "uniques": 1,
1921                "cost": view["stat"]["availableRUB"],
1922                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1923            },
1924            "Currencies": {
1925                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
1926                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
1927                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1928            },
1929            "Shares": {
1930                "uniques": len(view["stat"]["Shares"]),
1931                "cost": view["stat"]["sharesCostRUB"],
1932                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1933            },
1934            "Bonds": {
1935                "uniques": len(view["stat"]["Bonds"]),
1936                "cost": view["stat"]["bondsCostRUB"],
1937                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1938            },
1939            "Etfs": {
1940                "uniques": len(view["stat"]["Etfs"]),
1941                "cost": view["stat"]["etfsCostRUB"],
1942                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1943            },
1944            "Futures": {
1945                "uniques": len(view["stat"]["Futures"]),
1946                "cost": view["stat"]["futuresCostRUB"],
1947                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1948            },
1949        }
1950
1951        # portfolio distribution by companies:
1952        view["analytics"]["distrByCompanies"]["All money cash"] = {
1953            "ticker": "",
1954            "cost": view["stat"]["allCurrenciesCostRUB"],
1955            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1956        }
1957        view["analytics"]["distrByCompanies"].update(byComp)
1958
1959        # portfolio distribution by sectors:
1960        view["analytics"]["distrBySectors"]["All money cash"] = {
1961            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
1962            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
1963        }
1964        view["analytics"]["distrBySectors"].update(bySect)
1965
1966        # portfolio distribution by currencies:
1967        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
1968            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
1969
1970            if self.moreDebug:
1971                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
1972
1973        view["analytics"]["distrByCurrencies"].update(byCurr)
1974        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
1975        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
1976
1977        # portfolio distribution by countries:
1978        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
1979            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
1980
1981            if self.moreDebug:
1982                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
1983
1984        view["analytics"]["distrByCountries"].update(byCountry)
1985        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
1986        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
1987
1988        # --- Prepare text statistics overview in human-readable:
1989        if show:
1990            # Whatever the value `details`, header not changes:
1991            info = [
1992                "# Client's portfolio\n\n",
1993                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1994                "* **Account ID:** [{}]\n".format(self.accountId),
1995            ]
1996
1997            if details in ["full", "positions", "digest"]:
1998                info.extend([
1999                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2000                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2001                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2002                        view["stat"]["totalChangesRUB"],
2003                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2004                        view["stat"]["totalChangesPercentRUB"],
2005                    ),
2006                ])
2007
2008            if details in ["full", "positions"]:
2009                info.extend([
2010                    "## Open positions\n\n",
2011                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2012                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2013                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2014                        "{:.2f} ({:.2f}) rub".format(
2015                            view["stat"]["availableRUB"],
2016                            view["stat"]["blockedRUB"],
2017                        )
2018                    )
2019                ])
2020
2021                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2022                    return [
2023                        "|                             |                                 |          |              |              |                     |                              |\n",
2024                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2025                            noTradeStr if noTradeStr else typeStr,
2026                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2027                        ),
2028                    ]
2029
2030                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2031                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2032                        "{} [{}]".format(data["ticker"], data["figi"]),
2033                        "{:.2f} ({:.2f}) {}".format(
2034                            data["volume"],
2035                            data["blocked"],
2036                            data["currency"],
2037                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2038                            data["volume"],
2039                            data["blocked"],
2040                        ),
2041                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2042                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2043                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2044                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2045                        "{}{:.2f} {} ({}{:.2f}%)".format(
2046                            "+" if data["profit"] > 0 else "",
2047                            data["profit"], data["baseCurrencyName"],
2048                            "+" if data["percentProfit"] > 0 else "",
2049                            data["percentProfit"],
2050                        ),
2051                    )
2052
2053                # --- Show currencies section:
2054                if view["stat"]["Currencies"]:
2055                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2056                    for item in view["stat"]["Currencies"]:
2057                        info.append(_InfoStr(item, showCurrencyName=True))
2058
2059                else:
2060                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2061
2062                # --- Show shares section:
2063                if view["stat"]["Shares"]:
2064                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2065
2066                    for item in view["stat"]["Shares"]:
2067                        info.append(_InfoStr(item))
2068
2069                else:
2070                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2071
2072                # --- Show bonds section:
2073                if view["stat"]["Bonds"]:
2074                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2075
2076                    for item in view["stat"]["Bonds"]:
2077                        info.append(_InfoStr(item))
2078
2079                else:
2080                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2081
2082                # --- Show etfs section:
2083                if view["stat"]["Etfs"]:
2084                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2085
2086                    for item in view["stat"]["Etfs"]:
2087                        info.append(_InfoStr(item))
2088
2089                else:
2090                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2091
2092                # --- Show futures section:
2093                if view["stat"]["Futures"]:
2094                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2095
2096                    for item in view["stat"]["Futures"]:
2097                        info.append(_InfoStr(item))
2098
2099                else:
2100                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2101
2102            if details in ["full", "orders"]:
2103                # --- Show pending orders section:
2104                if view["stat"]["orders"]:
2105                    info.extend([
2106                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2107                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2108                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2109                    ])
2110
2111                    for item in view["stat"]["orders"]:
2112                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2113                            "{} [{}]".format(item["ticker"], item["figi"]),
2114                            item["orderID"],
2115                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2116                            "{} {} ({}{:.2f}%)".format(
2117                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2118                                item["baseCurrencyName"],
2119                                "+" if item["percentChanges"] > 0 else "",
2120                                float(item["percentChanges"]),
2121                            ),
2122                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2123                            item["action"],
2124                            item["type"],
2125                            item["date"],
2126                        ))
2127
2128                else:
2129                    info.append("\n## Total pending limit-orders: 0\n")
2130
2131                # --- Show stop orders section:
2132                if view["stat"]["stopOrders"]:
2133                    info.extend([
2134                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2135                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2136                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2137                    ])
2138
2139                    for item in view["stat"]["stopOrders"]:
2140                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2141                            "{} [{}]".format(item["ticker"], item["figi"]),
2142                            item["orderID"],
2143                            item["lotsRequested"],
2144                            "{} {} ({}{:.2f}%)".format(
2145                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2146                                item["baseCurrencyName"],
2147                                "+" if item["percentChanges"] > 0 else "",
2148                                float(item["percentChanges"]),
2149                            ),
2150                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2151                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2152                            item["action"],
2153                            item["type"],
2154                            item["expType"],
2155                            item["createDate"],
2156                            item["expDate"],
2157                        ))
2158
2159                else:
2160                    info.append("\n## Total stop-orders: 0\n")
2161
2162            if details in ["full", "analytics"]:
2163                # -- Show analytics section:
2164                if view["stat"]["portfolioCostRUB"] > 0:
2165                    info.extend([
2166                        "\n# Analytics\n"
2167                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2168                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2169                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2170                            view["stat"]["totalChangesRUB"],
2171                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2172                            view["stat"]["totalChangesPercentRUB"],
2173                        ),
2174                        "\n## Portfolio distribution by assets\n"
2175                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2176                        "|------------------------------------|---------|---------|--------------------|\n",
2177                    ])
2178
2179                    for key in view["analytics"]["distrByAssets"].keys():
2180                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2181                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2182                                key,
2183                                view["analytics"]["distrByAssets"][key]["uniques"],
2184                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2185                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2186                            ))
2187
2188                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2189
2190                    info.extend([
2191                        "\n## Portfolio distribution by companies\n"
2192                        "\n| Company                                      | Percent | Current cost       |\n",
2193                        aSepLine,
2194                    ])
2195
2196                    for company in view["analytics"]["distrByCompanies"].keys():
2197                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2198                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2199                                "{}{}".format(
2200                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2201                                    company,
2202                                ),
2203                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2204                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2205                            ))
2206
2207                    info.extend([
2208                        "\n## Portfolio distribution by sectors\n"
2209                        "\n| Sector                                       | Percent | Current cost       |\n",
2210                        aSepLine,
2211                    ])
2212
2213                    for sector in view["analytics"]["distrBySectors"].keys():
2214                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2215                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2216                                sector,
2217                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2218                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2219                            ))
2220
2221                    info.extend([
2222                        "\n## Portfolio distribution by currencies\n"
2223                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2224                        aSepLine,
2225                    ])
2226
2227                    for curr in view["analytics"]["distrByCurrencies"].keys():
2228                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2229                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2230                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2231                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2232                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2233                            ))
2234
2235                    info.extend([
2236                        "\n## Portfolio distribution by countries\n"
2237                        "\n| Assets by country                            | Percent | Current cost       |\n",
2238                        aSepLine,
2239                    ])
2240
2241                    for country in view["analytics"]["distrByCountries"].keys():
2242                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2243                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2244                                country,
2245                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2246                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2247                            ))
2248
2249            if details in ["full", "calendar"]:
2250                # -- Show bonds payment calendar section:
2251                if view["stat"]["Bonds"]:
2252                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2253                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2254                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2255
2256                else:
2257                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2258
2259            infoText = "".join(info)
2260
2261            uLogger.info(infoText)
2262
2263            if details == "full" and self.overviewFile:
2264                filename = self.overviewFile
2265
2266            elif details == "digest" and self.overviewDigestFile:
2267                filename = self.overviewDigestFile
2268
2269            elif details == "positions" and self.overviewPositionsFile:
2270                filename = self.overviewPositionsFile
2271
2272            elif details == "orders" and self.overviewOrdersFile:
2273                filename = self.overviewOrdersFile
2274
2275            elif details == "analytics" and self.overviewAnalyticsFile:
2276                filename = self.overviewAnalyticsFile
2277
2278            elif details == "calendar" and self.overviewBondsCalendarFile:
2279                filename = self.overviewBondsCalendarFile
2280
2281            else:
2282                filename = ""
2283
2284            if filename:
2285                with open(filename, "w", encoding="UTF-8") as fH:
2286                    fH.write(infoText)
2287
2288                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2289
2290        return view

Get portfolio: all open positions, orders and some statistics for current accountId. If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile and overviewBondsCalendarFile are defined then also save information to file.

WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.

Parameters
  • show: if False then only dictionary returns, if True then show more debug information.
  • details: how detailed should the information be?
    • full — shows full available information about portfolio status (by default),
    • positions — shows only open positions,
    • orders — shows only sections of open limits and stop orders.
    • digest — show a short digest of the portfolio status,
    • analytics — shows only the analytics section and the distribution of the portfolio by various categories,
    • calendar — shows only the bonds calendar section (if these present in portfolio),
Returns

dictionary with client's raw portfolio and some statistics.

def Deals( self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2292    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2293        """
2294        Returns history operations between two given dates for current `accountId`.
2295        If `reportFile` string is not empty then also save human-readable report.
2296        Shows some statistical data of closed positions.
2297
2298        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2299        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2300        :param show: if `True` then also prints all records to the console.
2301        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2302        :return: original list of dictionaries with history of deals records from API ("operations" key):
2303                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2304                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2305        """
2306        if self.accountId is None or not self.accountId:
2307            uLogger.error("Variable `accountId` must be defined for using this method!")
2308            raise Exception("Account ID required")
2309
2310        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2311
2312        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2313
2314        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2315        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2316        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2317        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2318        customStat = {}  # custom statistics in additional to responseJSON
2319
2320        # --- output report in human-readable format:
2321        if show or self.reportFile:
2322            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2323            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2324            nextDay = ""
2325
2326            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2327
2328            if len(ops) > 0:
2329                customStat = {
2330                    "opsCount": 0,  # total operations count
2331                    "buyCount": 0,  # buy operations
2332                    "sellCount": 0,  # sell operations
2333                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2334                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2335                    "payIn": {"rub": 0.},  # Deposit brokerage account
2336                    "payOut": {"rub": 0.},  # Withdrawals
2337                    "divs": {"rub": 0.},  # Dividends income
2338                    "coupons": {"rub": 0.},  # Coupon's income
2339                    "brokerCom": {"rub": 0.},  # Service commissions
2340                    "serviceCom": {"rub": 0.},  # Service commissions
2341                    "marginCom": {"rub": 0.},  # Margin commissions
2342                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2343                }
2344
2345                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2346                for item in ops:
2347                    if item["state"] == "OPERATION_STATE_EXECUTED":
2348                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2349
2350                        # count buy operations:
2351                        if "_BUY" in item["operationType"]:
2352                            customStat["buyCount"] += 1
2353
2354                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2355                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2356
2357                            else:
2358                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2359
2360                        # count sell operations:
2361                        elif "_SELL" in item["operationType"]:
2362                            customStat["sellCount"] += 1
2363
2364                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2365                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2366
2367                            else:
2368                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2369
2370                        # count incoming operations:
2371                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2372                            if item["payment"]["currency"] in customStat["payIn"].keys():
2373                                customStat["payIn"][item["payment"]["currency"]] += payment
2374
2375                            else:
2376                                customStat["payIn"][item["payment"]["currency"]] = payment
2377
2378                        # count withdrawals operations:
2379                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2380                            if item["payment"]["currency"] in customStat["payOut"].keys():
2381                                customStat["payOut"][item["payment"]["currency"]] += payment
2382
2383                            else:
2384                                customStat["payOut"][item["payment"]["currency"]] = payment
2385
2386                        # count dividends income:
2387                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2388                            if item["payment"]["currency"] in customStat["divs"].keys():
2389                                customStat["divs"][item["payment"]["currency"]] += payment
2390
2391                            else:
2392                                customStat["divs"][item["payment"]["currency"]] = payment
2393
2394                        # count coupon's income:
2395                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2396                            if item["payment"]["currency"] in customStat["coupons"].keys():
2397                                customStat["coupons"][item["payment"]["currency"]] += payment
2398
2399                            else:
2400                                customStat["coupons"][item["payment"]["currency"]] = payment
2401
2402                        # count broker commissions:
2403                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2404                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2405                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2406
2407                            else:
2408                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2409
2410                        # count service commissions:
2411                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2412                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2413                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2414
2415                            else:
2416                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2417
2418                        # count margin commissions:
2419                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2420                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2421                                customStat["marginCom"][item["payment"]["currency"]] += payment
2422
2423                            else:
2424                                customStat["marginCom"][item["payment"]["currency"]] = payment
2425
2426                        # count withholding taxes:
2427                        elif "_TAX" in item["operationType"]:
2428                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2429                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2430
2431                            else:
2432                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2433
2434                        else:
2435                            continue
2436
2437                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2438
2439                # --- view "Actions" lines:
2440                info.extend([
2441                    "| Report sections            |                               |                              |                      |                        |\n",
2442                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2443                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2444                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2445                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2446                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2447                    ),
2448                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2449                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2450                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2451                    ),
2452                ])
2453
2454                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2455                for key in opsKeys:
2456                    if key == "rub":
2457                        continue
2458
2459                    info.extend([
2460                        "|                            |                               | {:<28} |                      |                        |\n".format(
2461                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2462                        ),
2463                        "|                            |                               | {:<28} |                      |                        |\n".format(
2464                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2465                        ),
2466                    ])
2467
2468                info.append(splitLine1)
2469
2470                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2471                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2472                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2473                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2474                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2475                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2476                    )
2477
2478                # --- view "Payments" lines:
2479                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2480                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2481
2482                for key in paymentsKeys:
2483                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2484
2485                info.append(splitLine1)
2486
2487                # --- view "Commissions and taxes" lines:
2488                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2489                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2490
2491                for key in comKeys:
2492                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2493
2494                info.append(splitLine1)
2495
2496                info.extend([
2497                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2498                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2499                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2500                ])
2501
2502            else:
2503                info.append("Broker returned no operations during this period\n")
2504
2505            # --- view "Operations" section:
2506            for item in ops:
2507                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2508                    continue
2509
2510                else:
2511                    self.figi = item["figi"] if item["figi"] else ""
2512                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2513                    instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {}
2514
2515                    # group of deals during one day:
2516                    if nextDay and item["date"].split("T")[0] != nextDay:
2517                        info.append(splitLine2)
2518                        nextDay = ""
2519
2520                    else:
2521                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2522
2523                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2524                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2525                        self.figi if self.figi else "—",
2526                        instrument["ticker"] if instrument else "—",
2527                        instrument["type"] if instrument else "—",
2528                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2529                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2530                        TKS_OPERATION_STATES[item["state"]],
2531                        TKS_OPERATION_TYPES[item["operationType"]],
2532                    ))
2533
2534            infoText = "".join(info)
2535
2536            if show:
2537                if self.moreDebug:
2538                    uLogger.debug("Records about history of a client's operations successfully received")
2539
2540                uLogger.info(infoText)
2541
2542            if self.reportFile:
2543                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2544                    fH.write(infoText)
2545
2546                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2547
2548        return ops, customStat

Returns history operations between two given dates for current accountId. If reportFile string is not empty then also save human-readable report. Shows some statistical data of closed positions.

Parameters
  • start: see docstring in TradeRoutines.GetDatesAsString() method.
  • end: see docstring in TradeRoutines.GetDatesAsString() method.
  • show: if True then also prints all records to the console.
  • showCancelled: if False then remove information about cancelled operations from the deals report.
Returns

original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.

def History( self, start: str = None, end: str = None, interval: str = 'hour', onlyMissing: bool = False, csvSep: str = ',', show: bool = False) -> pandas.core.frame.DataFrame:
2550    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2551        """
2552        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2553
2554        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2555        Warning! Broker server used ISO UTC time by default.
2556
2557        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2558        Also, `historyFile` used to update history with `onlyMissing` parameter.
2559
2560        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2561
2562        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2563        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2564        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2565                         `"hour"`, `"day"`. Default: `"hour"`.
2566        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2567                            False by default. Warning! History appends only from last candle to current time
2568                            with always update last candle!
2569        :param csvSep: separator if csv-file is used, `,` by default.
2570        :param show: if `True` then also prints Pandas DataFrame to the console.
2571        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2572                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2573        """
2574        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2575        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2576        history = None  # empty pandas object for history
2577
2578        if interval not in TKS_CANDLE_INTERVALS.keys():
2579            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2580            raise Exception("Incorrect value")
2581
2582        if not (self.ticker or self.figi):
2583            uLogger.error("Ticker or FIGI must be defined!")
2584            raise Exception("Ticker or FIGI required")
2585
2586        if self.ticker and not self.figi:
2587            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2588            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2589
2590        if self.figi and not self.ticker:
2591            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2592            self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2593
2594        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2595        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2596        if interval.lower() != "day":
2597            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2598
2599        delta = dtEnd - dtStart  # current UTC time minus last time in file
2600        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2601
2602        # calculate history length in candles:
2603        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2604        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2605            length += 1  # to avoid fraction time
2606
2607        # calculate data blocks count:
2608        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2609
2610        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2611        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2612        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2613        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2614        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi))
2615
2616        tempOld = None  # pandas object for old history, if --only-missing key present
2617        lastTime = None  # datetime object of last old candle in file
2618
2619        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2620            uLogger.debug("--only-missing key present, add only last missing candles...")
2621            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2622
2623            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2624
2625            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2626            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2627            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2628            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2629
2630            # get last datetime object from last string in file or minus 1 delta if file is empty:
2631            if len(tempOld) > 0:
2632                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2633
2634            else:
2635                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2636
2637            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2638
2639        responseJSONs = []  # raw history blocks of data
2640
2641        blockEnd = dtEnd
2642        for item in range(blocks):
2643            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2644            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2645
2646            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2647                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2648            ))
2649
2650            if blockStart == blockEnd:
2651                uLogger.debug("Skipped this zero-length block...")
2652
2653            else:
2654                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2655                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2656                self.body = str({
2657                    "figi": self.figi,
2658                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2659                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2660                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2661                })
2662                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2663
2664                if "code" in responseJSON.keys():
2665                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2666
2667                else:
2668                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2669                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2670
2671                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2672
2673            blockEnd = blockStart
2674
2675        printCount = len(responseJSONs)  # candles to show in console
2676        if responseJSONs:
2677            tempHistory = pd.DataFrame(
2678                data={
2679                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2680                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2681                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2682                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2683                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2684                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2685                    "volume": [int(item["volume"]) for item in responseJSONs],
2686                },
2687                index=range(len(responseJSONs)),
2688                columns=["date", "time", "open", "high", "low", "close", "volume"],
2689            )
2690            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2691            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2692
2693            # append only newest candles to old history if --only-missing key present:
2694            if onlyMissing and tempOld is not None and lastTime is not None:
2695                index = 0  # find start index in tempHistory data:
2696
2697                for i, item in tempHistory.iterrows():
2698                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2699
2700                    if curTime == lastTime:
2701                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2702                        index = i
2703                        printCount = index + 1
2704                        break
2705
2706                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2707
2708            else:
2709                history = tempHistory  # if no `--only-missing` key then load full data from server
2710
2711            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2712
2713        if history is not None and not history.empty:
2714            if show:
2715                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2716                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2717                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2718                ))
2719
2720        else:
2721            uLogger.warning("Received an empty candles history!")
2722
2723        if self.historyFile is not None:
2724            if history is not None and not history.empty:
2725                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2726                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile)))
2727
2728            else:
2729                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2730
2731        else:
2732            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2733
2734        return history

This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).

History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01. Warning! Broker server used ISO UTC time by default.

If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame. Also, historyFile used to update history with onlyMissing parameter.

See also: LoadHistory() and ShowHistoryChart() methods.

Parameters
  • start: see docstring in TradeRoutines.GetDatesAsString() method.
  • end: see docstring in TradeRoutines.GetDatesAsString() method.
  • interval: this is a candle interval. Current available values are "1min", "5min", "15min", "hour", "day". Default: "hour".
  • onlyMissing: if True then add only last missing candles, do not request all history length from start. False by default. Warning! History appends only from last candle to current time with always update last candle!
  • csvSep: separator if csv-file is used, , by default.
  • show: if True then also prints Pandas DataFrame to the console.
Returns

Pandas DataFrame with prices history. Headers of columns are defined by default: ["date", "time", "open", "high", "low", "close", "volume"].

def LoadHistory(self, filePath: str) -> pandas.core.frame.DataFrame:
2736    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2737        """
2738        Load candles history from csv-file and return Pandas DataFrame object.
2739
2740        See also: `History()` and `ShowHistoryChart()` methods.
2741
2742        :param filePath: path to csv-file to open.
2743        """
2744        loadedHistory = None  # init candles data object
2745
2746        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2747
2748        if os.path.exists(filePath):
2749            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2750
2751            tfStr = self.priceModel.FormattedDelta(
2752                self.priceModel.timeframe,
2753                "{days} days {hours}h {minutes}m {seconds}s",
2754            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2755                self.priceModel.timeframe,
2756                "{hours}h {minutes}m {seconds}s",
2757            )
2758
2759            if loadedHistory is not None and not loadedHistory.empty:
2760                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2761                    len(loadedHistory),
2762                    tfStr,
2763                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2764                )
2765
2766            else:
2767                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2768
2769        else:
2770            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2771
2772        return loadedHistory

Load candles history from csv-file and return Pandas DataFrame object.

See also: History() and ShowHistoryChart() methods.

Parameters
  • filePath: path to csv-file to open.
def ShowHistoryChart( self, candles: Union[str, pandas.core.frame.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2774    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2775        """
2776        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2777
2778        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2779        Default: `index.html` (both for interact and non-interact candlesticks chart).
2780
2781        See also: `History()` and `LoadHistory()` methods.
2782
2783        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2784        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2785                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2786                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2787                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2788        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2789                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2790        """
2791        if isinstance(candles, str):
2792            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2793            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2794
2795        elif isinstance(candles, pd.DataFrame):
2796            self.priceModel.prices = candles  # set candles chain from variable
2797            self.priceModel.ticker = self.ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2798
2799            if "datetime" not in candles.columns:
2800                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2801
2802        else:
2803            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2804            raise Exception("Incorrect value")
2805
2806        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2807
2808        if interact:
2809            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2810
2811            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2812
2813        else:
2814            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2815
2816            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2817
2818        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))

Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.

Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart. Default: index.html (both for interact and non-interact candlesticks chart).

See also: History() and LoadHistory() methods.

Parameters
def Trade( self, operation: str, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2820    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2821        """
2822        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2823        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2824
2825        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2826
2827        :param operation: string "Buy" or "Sell".
2828        :param lots: volume, integer count of lots >= 1.
2829        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2830        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2831        :param expDate: string "Undefined" by default or local date in future,
2832                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2833        :return: JSON with response from broker server.
2834        """
2835        if self.accountId is None or not self.accountId:
2836            uLogger.error("Variable `accountId` must be defined for using this method!")
2837            raise Exception("Account ID required")
2838
2839        if operation is None or not operation or operation not in ("Buy", "Sell"):
2840            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2841            raise Exception("Incorrect value")
2842
2843        if lots is None or lots < 1:
2844            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2845            lots = 1
2846
2847        if tp is None or tp < 0:
2848            tp = 0
2849
2850        if sl is None or sl < 0:
2851            sl = 0
2852
2853        if expDate is None or not expDate:
2854            expDate = "Undefined"
2855
2856        if not (self.ticker or self.figi):
2857            uLogger.error("Ticker or FIGI must be defined!")
2858            raise Exception("Ticker or FIGI required")
2859
2860        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
2861        self.ticker = instrument["ticker"]
2862        self.figi = instrument["figi"]
2863
2864        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate))
2865
2866        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2867        self.body = str({
2868            "figi": self.figi,
2869            "quantity": str(lots),
2870            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2871            "accountId": str(self.accountId),
2872            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2873        })
2874        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
2875
2876        if "orderId" in response.keys():
2877            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2878                operation, response["orderId"],
2879                self.ticker, self.figi, lots,
2880                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2881                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2882                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2883            ))
2884
2885            if tp > 0:
2886                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2887
2888            if sl > 0:
2889                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2890
2891        else:
2892            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.")
2893
2894        return response

Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().

Parameters
  • operation: string "Buy" or "Sell".
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter targetPrice in self.Order().
  • sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter targetPrice in self.Order().
  • expDate: string "Undefined" by default or local date in future, it is a string with format %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Buy( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2896    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2897        """
2898        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
2899        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
2900
2901        See also: `Order()` and `Trade()` docstrings.
2902
2903        :param lots: volume, integer count of lots >= 1.
2904        :param tp: float > 0, take profit price of stop-order.
2905        :param sl: float > 0, stop loss price of stop-order.
2906        :param expDate: it's a local date in future.
2907                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2908        :return: JSON with response from broker server.
2909        """
2910        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Sell( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2912    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2913        """
2914        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
2915        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2916
2917        See also: `Order()` and `Trade()` docstrings.
2918
2919        :param lots: volume, integer count of lots >= 1.
2920        :param tp: float > 0, take profit price of stop-order.
2921        :param sl: float > 0, stop loss price of stop-order.
2922        :param expDate: it's a local date in the future.
2923                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2924        :return: JSON with response from broker server.
2925        """
2926        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in the future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
2928    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
2929        """
2930        Close position of given instruments.
2931
2932        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
2933        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
2934                         This avoids unnecessary downloading data from the server.
2935        """
2936        if instruments is None or not instruments:
2937            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
2938            raise Exception("Ticker or FIGI required")
2939
2940        if isinstance(instruments, str):
2941            instruments = [instruments]
2942
2943        uniqueInstruments = self.GetUniqueFIGIs(instruments)
2944        if uniqueInstruments:
2945            if portfolio is None or not portfolio:
2946                portfolio = self.Overview(show=False)
2947
2948            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
2949            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
2950
2951            for self.figi in uniqueInstruments:
2952                if self.figi not in allOpened:
2953                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi))
2954                    continue
2955
2956                # search open trade info about instrument by ticker:
2957                instrument = {}
2958                for iType in TKS_INSTRUMENTS:
2959                    if instrument:
2960                        break
2961
2962                    for item in portfolio["stat"][iType]:
2963                        if item["figi"] == self.figi:
2964                            instrument = item
2965                            break
2966
2967                if instrument:
2968                    self.ticker = instrument["ticker"]
2969                    self.figi = instrument["figi"]
2970
2971                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
2972                        self.ticker,
2973                        self.figi,
2974                        int(instrument["volume"]),
2975                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
2976                    ))
2977
2978                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
2979
2980                    if tradeLots > 0:
2981                        if instrument["blocked"] > 0:
2982                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
2983                                instrument["blocked"],
2984                                self.ticker,
2985                                tradeLots,
2986                            ))
2987
2988                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
2989                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
2990
2991                    else:
2992                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))

Close position of given instruments.

Parameters
  • instruments: list of instruments defined by tickers or FIGIs that must be closed.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
2994    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
2995        """
2996        Close all positions of given instruments with defined type.
2997
2998        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
2999        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3000                         This avoids unnecessary downloading data from the server.
3001        """
3002        if iType not in TKS_INSTRUMENTS:
3003            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3004
3005        else:
3006            if portfolio is None or not portfolio:
3007                portfolio = self.Overview(show=False)
3008
3009            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3010            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3011
3012            if tickers and portfolio:
3013                self.CloseTrades(tickers, portfolio)
3014
3015            else:
3016                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))

Close all positions of given instruments with defined type.

Parameters
  • iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def Order( self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3018    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3019        """
3020        Universal method to create market or limit orders with all available parameters for current `accountId`.
3021        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3022
3023        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3024        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3025
3026        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3027        then broker immediately open market order as you can do simple --buy or --sell operations!
3028
3029        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3030        When current price will go up or down to target price value then broker opens a limit order.
3031        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3032
3033        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3034
3035        :param operation: string "Buy" or "Sell".
3036        :param orderType: string "Limit" or "Stop".
3037        :param lots: volume, integer count of lots >= 1.
3038        :param targetPrice: target price > 0. This is open trade price for limit order.
3039        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3040                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3041        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3042                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3043                         Stop loss order always executed by market price.
3044        :param expDate: string "Undefined" by default or local date in future.
3045                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3046                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3047                        A limit order has no expiration date, it lasts until the end of the trading day.
3048        :return: JSON with response from broker server.
3049        """
3050        if self.accountId is None or not self.accountId:
3051            uLogger.error("Variable `accountId` must be defined for using this method!")
3052            raise Exception("Account ID required")
3053
3054        if operation is None or not operation or operation not in ("Buy", "Sell"):
3055            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3056            raise Exception("Incorrect value")
3057
3058        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3059            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3060            raise Exception("Incorrect value")
3061
3062        if lots is None or lots < 1:
3063            uLogger.error("You must define trade volume > 0: integer count of lots!")
3064            raise Exception("Incorrect value")
3065
3066        if targetPrice is None or targetPrice <= 0:
3067            uLogger.error("Target price for limit-order must be greater than 0!")
3068            raise Exception("Incorrect value")
3069
3070        if limitPrice is None or limitPrice <= 0:
3071            limitPrice = targetPrice
3072
3073        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3074            stopType = "Limit"
3075
3076        if expDate is None or not expDate:
3077            expDate = "Undefined"
3078
3079        if not (self.ticker or self.figi):
3080            uLogger.error("Tocker or FIGI must be defined!")
3081            raise Exception("Ticker or FIGI required")
3082
3083        response = {}
3084        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
3085        self.ticker = instrument["ticker"]
3086        self.figi = instrument["figi"]
3087
3088        if orderType == "Limit":
3089            uLogger.debug(
3090                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3091                    self.ticker, self.figi,
3092                    operation, lots, targetPrice, instrument["currency"],
3093                ))
3094
3095            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3096            self.body = str({
3097                "figi": self.figi,
3098                "quantity": str(lots),
3099                "price": FloatToNano(targetPrice),
3100                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3101                "accountId": str(self.accountId),
3102                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3103            })
3104            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3105
3106            if "orderId" in response.keys():
3107                uLogger.info(
3108                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3109                        response["orderId"],
3110                        self.ticker, self.figi,
3111                        operation, lots, targetPrice, instrument["currency"],
3112                    ))
3113
3114                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3115                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3116                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3117                            targetPrice, instrument["currency"],
3118                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3119                        ))
3120
3121                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3122                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3123                            targetPrice, instrument["currency"],
3124                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3125                        ))
3126
3127            else:
3128                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.")
3129
3130        if orderType == "Stop":
3131            uLogger.debug(
3132                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3133                    self.ticker, self.figi,
3134                    operation, lots,
3135                    targetPrice, instrument["currency"],
3136                    limitPrice, instrument["currency"],
3137                    stopType, expDate,
3138                ))
3139
3140            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3141            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3142            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3143
3144            body = {
3145                "figi": self.figi,
3146                "quantity": str(lots),
3147                "price": FloatToNano(limitPrice),
3148                "stopPrice": FloatToNano(targetPrice),
3149                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3150                "accountId": str(self.accountId),
3151                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3152                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3153            }
3154
3155            if expDateUTC:
3156                body["expireDate"] = expDateUTC
3157
3158            self.body = str(body)
3159            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3160
3161            if "stopOrderId" in response.keys():
3162                uLogger.info(
3163                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3164                        response["stopOrderId"],
3165                        self.ticker, self.figi,
3166                        operation, lots,
3167                        targetPrice, instrument["currency"],
3168                        limitPrice, instrument["currency"],
3169                        TKS_STOP_ORDER_TYPES[stopOrderType],
3170                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3171                    ))
3172
3173                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3174                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3175                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3176                            targetPrice, instrument["currency"],
3177                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3178                        ))
3179
3180                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3181                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3182                            targetPrice, instrument["currency"],
3183                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3184                        ))
3185
3186            else:
3187                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.")
3188
3189        return response

Universal method to create market or limit orders with all available parameters for current accountId. See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().

If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.

Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!

If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.

Only one attempt and no retry for opens order. If network issue occurred you can create new request.

Parameters
  • operation: string "Buy" or "Sell".
  • orderType: string "Limit" or "Stop".
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
  • limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
  • stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns

JSON with response from broker server.

def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3191    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3192        """
3193        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3194        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3195        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3196        See also: `Order()` docstring.
3197
3198        :param lots: volume, integer count of lots >= 1.
3199        :param targetPrice: target price > 0. This is open trade price for limit order.
3200        :return: JSON with response from broker server.
3201        """
3202        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Buy limit-order (below current price). You must specify only 2 parameters: lots and target price to open buy limit-order. If you try to create buy limit-order above current price then broker immediately open Buy market order, such as if you do simple --buy operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def BuyStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3204    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3205        """
3206        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3207        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3208        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3209        target price value then broker opens a limit order. See also: `Order()` docstring.
3210
3211        :param lots: volume, integer count of lots >= 1.
3212        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3213        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3214                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3215        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3216                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3217        :param expDate: string "Undefined" by default or local date in future.
3218                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3219                        This date is converting to UTC format for server.
3220        :return: JSON with response from broker server.
3221        """
3222        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order. In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for buy stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def SellLimit(self, lots: int, targetPrice: float) -> dict:
3224    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3225        """
3226        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3227        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3228        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3229        See also: `Order()` docstring.
3230
3231        :param lots: volume, integer count of lots >= 1.
3232        :param targetPrice: target price > 0. This is open trade price for limit order.
3233        :return: JSON with response from broker server.
3234        """
3235        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Sell limit-order (above current price). You must specify only 2 parameters: lots and target price to open sell limit-order. If you try to create sell limit-order below current price then broker immediately open Sell market order, such as if you do simple --sell operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def SellStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3237    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3238        """
3239        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3240        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3241        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3242        target price value then broker opens a limit order. See also: `Order()` docstring.
3243
3244        :param lots: volume, integer count of lots >= 1.
3245        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3246        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3247                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3248        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3249                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3250        :param expDate: string "Undefined" by default or local date in future.
3251                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3252                        This date is converting to UTC format for server.
3253        :return: JSON with response from broker server.
3254        """
3255        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order. In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for sell stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def CloseOrders( self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3257    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3258        """
3259        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3260
3261        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3262        :param allOrdersIDs: pre-received lists of all active pending orders.
3263                             This avoids unnecessary downloading data from the server.
3264        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3265        """
3266        if self.accountId is None or not self.accountId:
3267            uLogger.error("Variable `accountId` must be defined for using this method!")
3268            raise Exception("Account ID required")
3269
3270        if orderIDs:
3271            if allOrdersIDs is None or not allOrdersIDs:
3272                rawOrders = self.RequestPendingOrders()
3273                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3274
3275            if allStopOrdersIDs is None or not allStopOrdersIDs:
3276                rawStopOrders = self.RequestStopOrders()
3277                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3278
3279            for orderID in orderIDs:
3280                idInPendingOrders = orderID in allOrdersIDs
3281                idInStopOrders = orderID in allStopOrdersIDs
3282
3283                if not (idInPendingOrders or idInStopOrders):
3284                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3285                    continue
3286
3287                else:
3288                    if idInPendingOrders:
3289                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3290
3291                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3292                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3293                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3294                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3295
3296                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3297                            if self.moreDebug:
3298                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3299
3300                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3301
3302                        else:
3303                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3304
3305                    elif idInStopOrders:
3306                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3307
3308                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3309                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3310                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3311                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3312
3313                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3314                            if self.moreDebug:
3315                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3316
3317                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3318
3319                        else:
3320                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3321
3322                    else:
3323                        continue

Cancel order or list of orders by its orderId or stopOrderId for current accountId.

Parameters
  • orderIDs: list of integers with orderId or stopOrderId.
  • allOrdersIDs: pre-received lists of all active pending orders. This avoids unnecessary downloading data from the server.
  • allStopOrdersIDs: pre-received lists of all active stop orders.
def CloseAllOrders(self) -> None:
3325    def CloseAllOrders(self) -> None:
3326        """
3327        Gets a list of open pending and stop orders and cancel it all.
3328        """
3329        rawOrders = self.RequestPendingOrders()
3330        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3331        lenOrders = len(allOrdersIDs)
3332
3333        rawStopOrders = self.RequestStopOrders()
3334        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3335        lenSOrders = len(allStopOrdersIDs)
3336
3337        if lenOrders > 0 or lenSOrders > 0:
3338            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3339
3340            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3341
3342        else:
3343            uLogger.info("Orders not found, nothing to cancel.")

Gets a list of open pending and stop orders and cancel it all.

def CloseAll(self, *args) -> None:
3345    def CloseAll(self, *args) -> None:
3346        """
3347        Close all available (not blocked) opened trades and orders.
3348
3349        Also, you can select one or more keywords case-insensitive:
3350        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3351
3352        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3353        """
3354        overview = self.Overview(show=False)  # get all open trades info
3355
3356        if len(args) == 0:
3357            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3358            self.CloseAllOrders()  # close all pending and stop orders
3359
3360            for iType in TKS_INSTRUMENTS:
3361                if iType != "Currencies":
3362                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3363
3364        else:
3365            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3366            lowerArgs = [x.lower() for x in args]
3367
3368            if "orders" in lowerArgs:
3369                self.CloseAllOrders()  # close all pending and stop orders
3370
3371            for iType in TKS_INSTRUMENTS:
3372                if iType.lower() in lowerArgs and iType != "Currencies":
3373                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies

Close all available (not blocked) opened trades and orders.

Also, you can select one or more keywords case-insensitive: orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.

Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.

@staticmethod
def ParseOrderParameters(operation, **inputParameters)
3375    @staticmethod
3376    def ParseOrderParameters(operation, **inputParameters):
3377        """
3378        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3379
3380        :param operation: string "Buy" or "Sell".
3381        :param inputParameters: this is dict of strings that looks like this
3382               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3383               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3384               "prices" key: one or more prices to open limit-orders
3385               Counts of values in lots and prices lists must be equals!
3386        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3387        """
3388        # TODO: update order grid work with api v2
3389        pass
3390        # uLogger.debug("Input parameters: {}".format(inputParameters))
3391        #
3392        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3393        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3394        #     raise Exception("Incorrect value")
3395        #
3396        # if "l" in inputParameters.keys():
3397        #     inputParameters["lots"] = inputParameters.pop("l")
3398        #
3399        # if "p" in inputParameters.keys():
3400        #     inputParameters["prices"] = inputParameters.pop("p")
3401        #
3402        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3403        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3404        #     raise Exception("Incorrect value")
3405        #
3406        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3407        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3408        #
3409        # if len(lots) != len(prices):
3410        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3411        #     raise Exception("Incorrect value")
3412        #
3413        # uLogger.debug("Extracted parameters for orders:")
3414        # uLogger.debug("lots = {}".format(lots))
3415        # uLogger.debug("prices = {}".format(prices))
3416        #
3417        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3418        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3419        # uLogger.debug("Order parameters: {}".format(result))
3420        #
3421        # return result

Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.

Parameters
  • operation: string "Buy" or "Sell".
  • inputParameters: this is dict of strings that looks like this {"lots": "L_int,...", "prices": "P_float,..."} where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns

list of dictionaries with all lots and prices to open orders that looks like this [{"lot": lots_1, "price": price_1}, {...}, ...]

def IsInPortfolio(self, portfolio: dict = None) -> bool:
3423    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3424        """
3425        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3426
3427        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3428        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3429        """
3430        result = False
3431        msg = "Instrument not defined!"
3432
3433        if portfolio is None or not portfolio:
3434            portfolio = self.Overview(show=False)
3435
3436        if self.ticker:
3437            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3438            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3439
3440            for iType in TKS_INSTRUMENTS:
3441                for instrument in portfolio["stat"][iType]:
3442                    if instrument["ticker"] == self.ticker:
3443                        result = True
3444                        msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker)
3445                        break
3446
3447        elif self.figi:
3448            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3449            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3450
3451            for iType in TKS_INSTRUMENTS:
3452                for instrument in portfolio["stat"][iType]:
3453                    if instrument["figi"] == self.figi:
3454                        result = True
3455                        msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi)
3456                        break
3457
3458        else:
3459            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3460
3461        uLogger.debug(msg)
3462
3463        return result

Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if portfolio contains open position with given instrument, False otherwise.

def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3465    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3466        """
3467        Returns instrument from the user's portfolio if it presents there.
3468        Instrument must be defined by `ticker` (highly priority) or `figi`.
3469
3470        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3471        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3472        """
3473        result = None
3474        msg = "Instrument not defined!"
3475
3476        if portfolio is None or not portfolio:
3477            portfolio = self.Overview(show=False)
3478
3479        if self.ticker:
3480            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self.ticker))
3481            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3482
3483            for iType in TKS_INSTRUMENTS:
3484                for instrument in portfolio["stat"][iType]:
3485                    if instrument["ticker"] == self.ticker:
3486                        result = instrument
3487                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"])
3488                        break
3489
3490        elif self.figi:
3491            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3492            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3493
3494            for iType in TKS_INSTRUMENTS:
3495                for instrument in portfolio["stat"][iType]:
3496                    if instrument["figi"] == self.figi:
3497                        result = instrument
3498                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi)
3499                        break
3500
3501        else:
3502            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3503
3504        uLogger.debug(msg)
3505
3506        return result

Returns instrument from the user's portfolio if it presents there. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

dict with instrument if portfolio contains open position with this instrument, None otherwise.

def RequestLimits(self) -> dict:
3508    def RequestLimits(self) -> dict:
3509        """
3510        Method for obtaining the available funds for withdrawal for current `accountId`.
3511
3512        See also:
3513        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3514        - `OverviewLimits()` method
3515
3516        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3517                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3518                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3519                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3520        """
3521        if self.accountId is None or not self.accountId:
3522            uLogger.error("Variable `accountId` must be defined for using this method!")
3523            raise Exception("Account ID required")
3524
3525        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3526
3527        self.body = str({"accountId": self.accountId})
3528        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3529        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3530
3531        if self.moreDebug:
3532            uLogger.debug("Records about available funds for withdrawal successfully received")
3533
3534        return rawLimits

Method for obtaining the available funds for withdrawal for current accountId.

See also:

Returns

dict with raw data from server that contains free funds for withdrawal. Example of dict: {"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Here money is an array of portfolio currency positions, blocked is an array of blocked currency positions of the portfolio and blockedGuarantee is locked money under collateral for futures.

def OverviewLimits(self, show: bool = False) -> dict:
3536    def OverviewLimits(self, show: bool = False) -> dict:
3537        """
3538        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3539
3540        See also: `RequestLimits()`.
3541
3542        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3543        :return: dict with raw parsed data from server and some calculated statistics about it.
3544        """
3545        if self.accountId is None or not self.accountId:
3546            uLogger.error("Variable `accountId` must be defined for using this method!")
3547            raise Exception("Account ID required")
3548
3549        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3550
3551        view = {
3552            "rawLimits": rawLimits,
3553            "limits": {  # parsed data for every currency:
3554                "money": {  # this is an array of portfolio currency positions
3555                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3556                },
3557                "blocked": {  # this is an array of blocked currency
3558                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3559                },
3560                "blockedGuarantee": {  # this is locked money under collateral for futures
3561                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3562                },
3563            },
3564        }
3565
3566        # --- Prepare text table with limits in human-readable format:
3567        if show:
3568            info = [
3569                "# Withdrawal limits\n\n",
3570                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3571                "* **Account ID:** [{}]\n".format(self.accountId),
3572            ]
3573
3574            if view["limits"]["money"]:
3575                info.extend([
3576                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3577                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3578                ])
3579
3580            else:
3581                info.append("\nNo withdrawal limits\n")
3582
3583            for curr in view["limits"]["money"].keys():
3584                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3585                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3586                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3587
3588                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3589                    "[{}]".format(curr),
3590                    "{:.2f}".format(view["limits"]["money"][curr]),
3591                    "{:.2f}".format(availableMoney),
3592                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3593                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3594                )
3595
3596                if curr == "rub":
3597                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3598
3599                else:
3600                    info.append(infoStr)
3601
3602            infoText = "".join(info)
3603
3604            uLogger.info(infoText)
3605
3606            if self.withdrawalLimitsFile:
3607                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3608                    fH.write(infoText)
3609
3610                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3611
3612        return view

Method for parsing and show table with available funds for withdrawal for current accountId.

See also: RequestLimits().

Parameters
  • show: if False then only dictionary returns, if True then also print withdrawal limits to log.
Returns

dict with raw parsed data from server and some calculated statistics about it.

def RequestAccounts(self) -> dict:
3614    def RequestAccounts(self) -> dict:
3615        """
3616        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3617
3618        See also:
3619        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3620        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3621        - `OverviewUserInfo()` method
3622
3623        :return: dict with raw data from server that contains accounts info. Example of dict:
3624                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3625                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3626                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3627                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3628        """
3629        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3630
3631        self.body = str({})
3632        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3633        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3634
3635        if self.moreDebug:
3636            uLogger.debug("Records about available accounts successfully received")
3637
3638        return rawAccounts

Method for requesting all brokerage accounts (accountIds) of current user detected by token.

See also:

Returns

dict with raw data from server that contains accounts info. Example of dict: {"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. If closedDate="1970-01-01T00:00:00Z" it means that account is active now.

def RequestUserInfo(self) -> dict:
3640    def RequestUserInfo(self) -> dict:
3641        """
3642        Method for requesting common user's information.
3643
3644        See also:
3645        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3646        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3647        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3648        - `OverviewUserInfo()` method
3649
3650        :return: dict with raw data from server that contains user's information. Example of dict:
3651                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3652                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3653        """
3654        uLogger.debug("Requesting common user's information. Wait, please...")
3655
3656        self.body = str({})
3657        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3658        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3659
3660        if self.moreDebug:
3661            uLogger.debug("Records about current user successfully received")
3662
3663        return rawUserInfo

Method for requesting common user's information.

See also:

Returns

dict with raw data from server that contains user's information. Example of dict: {"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.

def RequestMarginStatus(self, accountId: str = None) -> dict:
3665    def RequestMarginStatus(self, accountId: str = None) -> dict:
3666        """
3667        Method for requesting margin calculation for defined account ID.
3668
3669        See also:
3670        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3671        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3672        - `OverviewUserInfo()` method
3673
3674        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3675        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3676                 Example of responses:
3677                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3678                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3679                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3680                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3681                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3682                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3683        """
3684        if accountId is None or not accountId:
3685            if self.accountId is None or not self.accountId:
3686                uLogger.error("Variable `accountId` must be defined for using this method!")
3687                raise Exception("Account ID required")
3688
3689            else:
3690                accountId = self.accountId  # use `self.accountId` (main ID) by default
3691
3692        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3693
3694        self.body = str({"accountId": accountId})
3695        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3696        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3697
3698        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3699            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3700            rawMargin = {}
3701
3702        else:
3703            if self.moreDebug:
3704                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3705
3706        return rawMargin

Method for requesting margin calculation for defined account ID.

See also:

Parameters
  • accountId: string with numeric account ID. If None, then used class field accountId.
Returns

dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400: {"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns: {}. status code 200: {"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.

def RequestTariffLimits(self) -> dict:
3708    def RequestTariffLimits(self) -> dict:
3709        """
3710        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
3711
3712        See also:
3713        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
3714        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
3715        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
3716        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
3717        - `OverviewUserInfo()` method
3718
3719        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
3720                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
3721                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
3722        """
3723        uLogger.debug("Requesting limits of current tariff. Wait, please...")
3724
3725        self.body = str({})
3726        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
3727        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3728
3729        if self.moreDebug:
3730            uLogger.debug("Records with limits of current tariff successfully received")
3731
3732        return rawTariffLimits

Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.

See also:

Returns

dict with raw data from server that contains limits of current tariff. Example of dict: {"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.

def RequestBondCoupons(self, iJSON: dict) -> dict:
3734    def RequestBondCoupons(self, iJSON: dict) -> dict:
3735        """
3736        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
3737        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
3738        All dates are in UTC timezone.
3739
3740        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
3741        Documentation:
3742        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
3743        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
3744
3745        See also: `ExtendBondsData()`.
3746
3747        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]`
3748                      If raw iJSON is not data of bond then server returns an error [400] with message:
3749                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
3750        :return: dictionary with bond payment calendar. Response example
3751                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
3752                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
3753                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
3754                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
3755        """
3756        if iJSON["figi"] is None or not iJSON["figi"]:
3757            uLogger.error("FIGI must be defined for using this method!")
3758            raise Exception("FIGI required")
3759
3760        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
3761        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
3762
3763        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
3764            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
3765            self.figi,
3766            startDate,
3767            endDate,
3768        ))
3769
3770        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
3771        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
3772        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
3773
3774        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
3775            uLogger.warning("Instrument type is not bond!")
3776
3777        else:
3778            if self.moreDebug:
3779                uLogger.debug("Records about bond payment calendar successfully received")
3780
3781        return calendar

Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z". All dates are in UTC timezone.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:

See also: ExtendBondsData().

Parameters
  • iJSON: raw json data of a bond from broker server, example iJSON = self.iList["Bonds"][self.ticker] If raw iJSON is not data of bond then server returns an error [400] with message: {"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns

dictionary with bond payment calendar. Response example {"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}

def ExtendBondsData( self, instruments: list[str], xlsx: bool = False) -> pandas.core.frame.DataFrame:
3783    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
3784        """
3785        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
3786        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
3787        coupon yields, current yields and some statistics etc.
3788
3789        WARNING! This is too long operation if a lot of bonds requested from broker server.
3790
3791        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
3792
3793        :param instruments: list of strings with tickers or FIGIs.
3794        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
3795                     for further used by data scientists or stock analytics.
3796        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
3797                 In XLSX-file and Pandas DataFrame fields mean:
3798                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
3799                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
3800        """
3801        if instruments is None or not instruments:
3802            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3803            raise Exception("Ticker or FIGI required")
3804
3805        if isinstance(instruments, str):
3806            instruments = [instruments]
3807
3808        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3809
3810        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
3811
3812        iCount = len(uniqueInstruments)
3813        tooLong = iCount >= 20
3814        if tooLong:
3815            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
3816
3817        bonds = None
3818        for i, self.figi in enumerate(uniqueInstruments):
3819            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
3820
3821            if "type" in instrument.keys() and instrument["type"] == "Bonds":
3822                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
3823                rawBond = self.SearchByFIGI(requestPrice=True)
3824
3825                # Widen raw data with UTC current time (iData["actualDateTime"]):
3826                actualDate = datetime.now(tzutc())
3827                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
3828
3829                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
3830                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
3831
3832                # Replace some values with human-readable:
3833                iData["nominalCurrency"] = iData["nominal"]["currency"]
3834                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
3835                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
3836                iData["aciCurrency"] = iData["aciValue"]["currency"]
3837                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
3838                iData["issueSize"] = int(iData["issueSize"])
3839                iData["issueSizePlan"] = int(iData["issueSizePlan"])
3840                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
3841                iData["step"] = iData["step"] if "step" in iData.keys() else 0
3842                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
3843                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
3844                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
3845                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
3846                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
3847                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
3848                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
3849
3850                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
3851                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
3852                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
3853                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
3854                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
3855                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
3856                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
3857                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
3858                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
3859                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
3860                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
3861
3862                # Widen raw data with calendar data from `rawCalendar` values:
3863                calendarData = []
3864                if "events" in iData["rawCalendar"].keys():
3865                    for item in iData["rawCalendar"]["events"]:
3866                        calendarData.append({
3867                            "couponDate": item["couponDate"],
3868                            "couponNumber": int(item["couponNumber"]),
3869                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
3870                            "payCurrency": item["payOneBond"]["currency"],
3871                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
3872                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
3873                            "couponStartDate": item["couponStartDate"],
3874                            "couponEndDate": item["couponEndDate"],
3875                            "couponPeriod": item["couponPeriod"],
3876                        })
3877
3878                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
3879                    if "maturityDate" not in iData.keys():
3880                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
3881
3882                # Widen raw data with Coupon Rate.
3883                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
3884                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
3885                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
3886                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
3887
3888                # Widen raw data with Yield to Maturity (YTM) on current date.
3889                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
3890                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
3891                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
3892                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
3893                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
3894                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
3895
3896                iData["calendar"] = calendarData  # adds calendar at the end
3897
3898                # Remove not used data:
3899                iData.pop("uid")
3900                iData.pop("positionUid")
3901                iData.pop("currentPrice")
3902                iData.pop("rawCalendar")
3903
3904                colNames = list(iData.keys())
3905                if bonds is None:
3906                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
3907
3908                else:
3909                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
3910
3911            else:
3912                uLogger.warning("Instrument is not a bond!")
3913
3914            processed = round(100 * (i + 1) / iCount, 1)
3915            if tooLong and processed % 5 == 0:
3916                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
3917
3918            else:
3919                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
3920
3921        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
3922
3923        # Saving bonds from Pandas DataFrame to XLSX sheet:
3924        if xlsx and self.bondsXLSXFile:
3925            with pd.ExcelWriter(
3926                    path=self.bondsXLSXFile,
3927                    date_format=TKS_DATE_FORMAT,
3928                    datetime_format=TKS_DATE_TIME_FORMAT,
3929                    mode="w",
3930            ) as writer:
3931                bonds.to_excel(
3932                    writer,
3933                    sheet_name="Extended bonds data",
3934                    index=True,
3935                    encoding="UTF-8",
3936                    freeze_panes=(1, 1),
3937                )  # saving as XLSX-file with freeze first row and column as headers
3938
3939            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
3940
3941        return bonds

Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • xlsx: if True then also exports Pandas DataFrame to xlsx-file bondsXLSXFile, default ext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns

wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon

def CreateBondsCalendar( self, extBonds: pandas.core.frame.DataFrame, xlsx: bool = False) -> pandas.core.frame.DataFrame:
3943    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
3944        """
3945        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
3946
3947        WARNING! This is too long operation if a lot of bonds requested from broker server.
3948
3949        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
3950
3951        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
3952                        extended information about bonds: main info, current prices, bond payment calendar,
3953                        coupon yields, current yields and some statistics etc.
3954                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
3955        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
3956                     for further used by data scientists or stock analytics.
3957        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
3958        """
3959        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
3960            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
3961
3962        uLogger.debug("Generating bond payments calendar data. Wait, please...")
3963
3964        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
3965        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
3966        calendar = None
3967        for bond in extBonds.iterrows():
3968            for item in bond[1]["calendar"]:
3969                cData = {
3970                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
3971                    "couponDate": item["couponDate"],
3972                    "figi": bond[1]["figi"],
3973                    "ticker": bond[1]["ticker"],
3974                    "name": bond[1]["name"],
3975                    "couponNumber": item["couponNumber"],
3976                    "payOneBond": item["payOneBond"],
3977                    "payCurrency": item["payCurrency"],
3978                    "couponType": item["couponType"],
3979                    "couponPeriod": item["couponPeriod"],
3980                    "fixDate": item["fixDate"],
3981                    "couponStartDate": item["couponStartDate"],
3982                    "couponEndDate": item["couponEndDate"],
3983                }
3984
3985                if calendar is None:
3986                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
3987
3988                else:
3989                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
3990
3991        if calendar is not None:
3992            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
3993
3994            # Saving calendar from Pandas DataFrame to XLSX sheet:
3995            if xlsx:
3996                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
3997
3998                with pd.ExcelWriter(
3999                        path=xlsxCalendarFile,
4000                        date_format=TKS_DATE_FORMAT,
4001                        datetime_format=TKS_DATE_TIME_FORMAT,
4002                        mode="w",
4003                ) as writer:
4004                    humanReadable = calendar.copy(deep=True)
4005                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4006                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4007                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4008                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4009                    humanReadable.columns = colNames  # human-readable column names
4010
4011                    humanReadable.to_excel(
4012                        writer,
4013                        sheet_name="Bond payments calendar",
4014                        index=False,
4015                        encoding="UTF-8",
4016                        freeze_panes=(1, 2),
4017                    )  # saving as XLSX-file with freeze first row and column as headers
4018
4019                    del humanReadable  # release df in memory
4020
4021                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4022
4023        return calendar

Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowBondsCalendar(), ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • xlsx: if True then also exports Pandas DataFrame to file calendarFile + ".xlsx", calendar.xlsx by default, for further used by data scientists or stock analytics.
Returns

Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon

def ShowBondsCalendar(self, extBonds: pandas.core.frame.DataFrame, show: bool = True) -> str:
4025    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4026        """
4027        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4028        Also, creates Markdown file with calendar data, `calendar.md` by default.
4029
4030        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4031
4032        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4033                        extended information about bonds: main info, current prices, bond payment calendar,
4034                        coupon yields, current yields and some statistics etc.
4035                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4036        :param show: if `True` then also printing bonds payment calendar to the console,
4037                     otherwise save to file `calendarFile` only. `False` by default.
4038        :return: multilines text in Markdown format with bonds payment calendar as a table.
4039        """
4040        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4041            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4042
4043        infoText = "# Bond payments calendar\n\n"
4044
4045        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4046
4047        if not (calendar is None or calendar.empty):
4048            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4049
4050            info = [
4051                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4052                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4053            ]
4054
4055            newMonth = False
4056            notOneBond = calendar["figi"].nunique() > 1
4057            for i, bond in enumerate(calendar.iterrows()):
4058                if newMonth and notOneBond:
4059                    info.append(splitLine)
4060
4061                info.append(
4062                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4063                        "  √" if bond[1]["paid"] else "  —",
4064                        bond[1]["couponDate"].split("T")[0],
4065                        bond[1]["figi"],
4066                        bond[1]["ticker"],
4067                        bond[1]["couponNumber"],
4068                        "{} {}".format(
4069                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4070                            bond[1]["payCurrency"],
4071                        ),
4072                        bond[1]["couponType"],
4073                        bond[1]["couponPeriod"],
4074                        bond[1]["fixDate"].split("T")[0],
4075                    )
4076                )
4077
4078                if i < len(calendar.values) - 1:
4079                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4080                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4081                    newMonth = False if curDate.month == nextDate.month else True
4082
4083                else:
4084                    newMonth = False
4085
4086            infoText += "".join(info)
4087
4088            if show:
4089                uLogger.info("{}".format(infoText))
4090
4091            if self.calendarFile is not None:
4092                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4093                    fH.write(infoText)
4094
4095                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4096
4097        else:
4098            infoText += "No data\n"
4099
4100        return infoText

Show bond payments calendar as a table. One row in input bonds dataframe contains one bond. Also, creates Markdown file with calendar data, calendar.md by default.

See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • show: if True then also printing bonds payment calendar to the console, otherwise save to file calendarFile only. False by default.
Returns

multilines text in Markdown format with bonds payment calendar as a table.

def OverviewAccounts(self, show: bool = False) -> dict:
4102    def OverviewAccounts(self, show: bool = False) -> dict:
4103        """
4104        Method for parsing and show simple table with all available user accounts.
4105
4106        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4107
4108        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4109        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4110                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4111                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4112                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4113                                                        "closed": "—", "access": "Full access" }, ...}}`
4114        """
4115        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4116
4117        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4118        accounts = {
4119            item["id"]: {
4120                "type": TKS_ACCOUNT_TYPES[item["type"]],
4121                "name": item["name"],
4122                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4123                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4124                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4125                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4126            } for item in rawAccounts["accounts"]
4127        }
4128
4129        # Raw and parsed data with some fields replaced in "stat" section:
4130        view = {
4131            "rawAccounts": rawAccounts,
4132            "stat": accounts,
4133        }
4134
4135        # --- Prepare simple text table with only accounts data in human-readable format:
4136        if show:
4137            info = [
4138                "# User accounts\n\n",
4139                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4140                "| Account ID   | Type                      | Status                    | Name                           |\n",
4141                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4142            ]
4143
4144            for account in view["stat"].keys():
4145                info.extend([
4146                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4147                        account,
4148                        view["stat"][account]["type"],
4149                        view["stat"][account]["status"],
4150                        view["stat"][account]["name"],
4151                    )
4152                ])
4153
4154            infoText = "".join(info)
4155
4156            uLogger.info(infoText)
4157
4158            if self.userAccountsFile:
4159                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4160                    fH.write(infoText)
4161
4162                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4163
4164        return view

Method for parsing and show simple table with all available user accounts.

See also: RequestAccounts() and OverviewUserInfo() methods.

Parameters
  • show: if False then only dictionary with accounts data returns, if True then also print it to log.
Returns

dict with parsed accounts data received from RequestAccounts() method. Example of dict: view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}

def OverviewUserInfo(self, show: bool = False) -> dict:
4166    def OverviewUserInfo(self, show: bool = False) -> dict:
4167        """
4168        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4169
4170        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4171
4172        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4173        :return: dict with raw parsed data from server and some calculated statistics about it.
4174        """
4175        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4176        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4177        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4178        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4179        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4180        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4181
4182        # This is dict with parsed common user data:
4183        userInfo = {
4184            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4185            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4186            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4187            "tariff": rawUserInfo["tariff"],
4188        }
4189
4190        # This is an array of dict with parsed margin statuses for every account IDs:
4191        margins = {}
4192        for accountId in accounts.keys():
4193            if rawMargins[accountId]:
4194                margins[accountId] = {
4195                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4196                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4197                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4198                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4199                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4200                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4201                }
4202
4203            else:
4204                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4205
4206        unary = {}  # unary-connection limits
4207        for item in rawTariffLimits["unaryLimits"]:
4208            if item["limitPerMinute"] in unary.keys():
4209                unary[item["limitPerMinute"]].extend(item["methods"])
4210
4211            else:
4212                unary[item["limitPerMinute"]] = item["methods"]
4213
4214        stream = {}  # stream-connection limits
4215        for item in rawTariffLimits["streamLimits"]:
4216            if item["limit"] in stream.keys():
4217                stream[item["limit"]].extend(item["streams"])
4218
4219            else:
4220                stream[item["limit"]] = item["streams"]
4221
4222        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4223        limits = {
4224            "unary": unary,
4225            "stream": stream,
4226        }
4227
4228        # Raw and parsed data as an output result:
4229        view = {
4230            "rawUserInfo": rawUserInfo,
4231            "rawAccounts": rawAccounts,
4232            "rawMargins": rawMargins,
4233            "rawTariffLimits": rawTariffLimits,
4234            "stat": {
4235                "userInfo": userInfo,
4236                "accounts": accounts,
4237                "margins": margins,
4238                "limits": limits,
4239            },
4240        }
4241
4242        # --- Prepare text table with user information in human-readable format:
4243        if show:
4244            info = [
4245                "# Full user information\n\n",
4246                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4247                "## Common information\n\n",
4248                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4249                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4250                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4251                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4252                "\n## User accounts\n\n",
4253            ]
4254
4255            for account in view["stat"]["accounts"].keys():
4256                info.extend([
4257                    "### ID: [{}]\n\n".format(account),
4258                    "| Parameters           | Values                                                       |\n",
4259                    "|----------------------|--------------------------------------------------------------|\n",
4260                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4261                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4262                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4263                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4264                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4265                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4266                ])
4267
4268                if margins[account]:
4269                    info.extend([
4270                        "| Margin status:       | Enabled                                                      |\n",
4271                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4272                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4273                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4274                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4275                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4276                    ])
4277
4278                else:
4279                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4280
4281            info.extend([
4282                "\n## Current user tariff limits\n",
4283                "\nSee also:\n",
4284                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4285                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4286                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4287                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4288                "\n### Unary limits\n",
4289            ])
4290
4291            if unary:
4292                for key, values in sorted(unary.items()):
4293                    info.append("\n* Max requests per minute: {}\n".format(key))
4294
4295                    for value in values:
4296                        info.append("  - {}\n".format(value))
4297
4298            else:
4299                info.append("\nNot available\n")
4300
4301            info.append("\n### Stream limits\n")
4302
4303            if stream:
4304                for key, values in sorted(stream.items()):
4305                    info.append("\n* Max stream connections: {}\n".format(key))
4306
4307                    for value in values:
4308                        info.append("  - {}\n".format(value))
4309
4310            else:
4311                info.append("\nNot available\n")
4312
4313            infoText = "".join(info)
4314
4315            uLogger.info(infoText)
4316
4317            if self.userInfoFile:
4318                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4319                    fH.write(infoText)
4320
4321                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4322
4323        return view

Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).

See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.

Parameters
  • show: if False then only dictionary returns, if True then also print user's data to log.
Returns

dict with raw parsed data from server and some calculated statistics about it.

class Args:
4326class Args:
4327    """
4328    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4329    """
4330    def __init__(self, **kwargs):
4331        self.__dict__.update(kwargs)
4332
4333    def __getattr__(self, item):
4334        return None

If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.

Args(**kwargs)
4330    def __init__(self, **kwargs):
4331        self.__dict__.update(kwargs)
def ParseArgs()
4337def ParseArgs():
4338    """This function get and parse command line keys."""
4339    parser = ArgumentParser()  # command-line string parser
4340
4341    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4342    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4343
4344    # --- options:
4345
4346    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4347    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4348    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4349
4350    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4351    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4352
4353    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4354    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4355
4356    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4357
4358    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4359    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4360    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4361
4362    parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4363    parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.")
4364
4365    # --- commands:
4366
4367    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4368
4369    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4370    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4371    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4372    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4373    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4374    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4375    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4376    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4377
4378    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4379    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4380    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4381    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4382    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4383    parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.")
4384
4385    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4386    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4387    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4388    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4389
4390    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4391    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4392    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4393
4394    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4395    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4396    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4397    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4398    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4399    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4400    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4401
4402    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4403    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4404    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.")
4405    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.")
4406    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.")
4407
4408    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4409    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4410    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4411
4412    cmdArgs = parser.parse_args()
4413    return cmdArgs

This function get and parse command line keys.

def Main(**kwargs)
4416def Main(**kwargs):
4417    """
4418    Main function for work with TKSBrokerAPI in the console.
4419
4420    See examples:
4421    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4422    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4423    """
4424    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4425
4426    if args.debug_level:
4427        uLogger.level = 10  # always debug level by default
4428        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4429
4430    exitCode = 0
4431    start = datetime.now(tzutc())
4432    uLogger.debug("=-" * 50)
4433    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4434        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4435        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4436    ))
4437
4438    # trying to calculate full current version:
4439    buildVersion = __version__
4440    try:
4441        v = version("tksbrokerapi")
4442        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4443
4444    except Exception:
4445        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4446
4447    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4448    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4449
4450    try:
4451        if args.version:
4452            print("TKSBrokerAPI {}".format(buildVersion))
4453            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4454
4455        else:
4456            # Init class for trading with Tinkoff Broker:
4457            trader = TinkoffBrokerServer(
4458                token=args.token,
4459                accountId=args.account_id,
4460                useCache=not args.no_cache,
4461            )
4462
4463            # --- set some options:
4464
4465            if args.more:
4466                trader.moreDebug = True
4467                uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.")
4468
4469            if args.ticker:
4470                ticker = args.ticker.upper()  # Tickers may be upper case only
4471
4472                if ticker in trader.aliasesKeys:
4473                    trader.ticker = trader.aliases[ticker]  # Replace some tickers with its aliases
4474
4475                else:
4476                    trader.ticker = ticker
4477
4478            if args.figi:
4479                trader.figi = args.figi.upper()  # FIGIs may be upper case only
4480
4481            if args.depth is not None:
4482                trader.depth = args.depth
4483
4484            # --- do one command:
4485
4486            if args.list:
4487                if args.output is not None:
4488                    trader.instrumentsFile = args.output
4489
4490                trader.ShowInstrumentsInfo(show=True)
4491
4492            elif args.list_xlsx:
4493                trader.DumpInstrumentsAsXLSX(forceUpdate=False)
4494
4495            elif args.bonds_xlsx is not None:
4496                if args.output is not None:
4497                    trader.bondsXLSXFile = args.output
4498
4499                if len(args.bonds_xlsx) == 0:
4500                    trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4501
4502                else:
4503                    trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4504
4505            elif args.search:
4506                if args.output is not None:
4507                    trader.searchResultsFile = args.output
4508
4509                trader.SearchInstruments(pattern=args.search[0], show=True)
4510
4511            elif args.info:
4512                if not (args.ticker or args.figi):
4513                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4514                    raise Exception("Ticker or FIGI required")
4515
4516                if args.output is not None:
4517                    trader.infoFile = args.output
4518
4519                if args.ticker:
4520                    trader.SearchByTicker(requestPrice=True, show=True)  # show info and current prices by ticker name
4521
4522                else:
4523                    trader.SearchByFIGI(requestPrice=True, show=True)  # show info and current prices by FIGI id
4524
4525            elif args.calendar is not None:
4526                if args.output is not None:
4527                    trader.calendarFile = args.output
4528
4529                if len(args.calendar) == 0:
4530                    bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4531
4532                else:
4533                    bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4534
4535                trader.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4536
4537            elif args.price:
4538                if not (args.ticker or args.figi):
4539                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4540                    raise Exception("Ticker or FIGI required")
4541
4542                trader.GetCurrentPrices(show=True)
4543
4544            elif args.prices is not None:
4545                if args.output is not None:
4546                    trader.pricesFile = args.output
4547
4548                trader.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4549
4550            elif args.overview:
4551                if args.output is not None:
4552                    trader.overviewFile = args.output
4553
4554                trader.Overview(show=True, details="full")
4555
4556            elif args.overview_digest:
4557                if args.output is not None:
4558                    trader.overviewDigestFile = args.output
4559
4560                trader.Overview(show=True, details="digest")
4561
4562            elif args.overview_positions:
4563                if args.output is not None:
4564                    trader.overviewPositionsFile = args.output
4565
4566                trader.Overview(show=True, details="positions")
4567
4568            elif args.overview_orders:
4569                if args.output is not None:
4570                    trader.overviewOrdersFile = args.output
4571
4572                trader.Overview(show=True, details="orders")
4573
4574            elif args.overview_analytics:
4575                if args.output is not None:
4576                    trader.overviewAnalyticsFile = args.output
4577
4578                trader.Overview(show=True, details="analytics")
4579
4580            elif args.overview_calendar:
4581                if args.output is not None:
4582                    trader.overviewAnalyticsFile = args.output
4583
4584                trader.Overview(show=True, details="calendar")
4585
4586            elif args.deals is not None:
4587                if args.output is not None:
4588                    trader.reportFile = args.output
4589
4590                if 0 <= len(args.deals) < 3:
4591                    trader.Deals(
4592                        start=args.deals[0] if len(args.deals) >= 1 else None,
4593                        end=args.deals[1] if len(args.deals) == 2 else None,
4594                        show=True,  # Always show deals report in console
4595                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
4596                    )
4597
4598                else:
4599                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4600                    raise Exception("Incorrect value")
4601
4602            elif args.history is not None:
4603                if args.output is not None:
4604                    trader.historyFile = args.output
4605
4606                if 0 <= len(args.history) < 3:
4607                    dataReceived = trader.History(
4608                        start=args.history[0] if len(args.history) >= 1 else None,
4609                        end=args.history[1] if len(args.history) == 2 else None,
4610                        interval="hour" if args.interval is None or not args.interval else args.interval,
4611                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
4612                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
4613                        show=True,  # shows all downloaded candles in console
4614                    )
4615
4616                    if args.render_chart is not None and dataReceived is not None:
4617                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4618
4619                        trader.ShowHistoryChart(
4620                            candles=dataReceived,
4621                            interact=iChart,
4622                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4623                        )
4624
4625                else:
4626                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4627                    raise Exception("Incorrect value")
4628
4629            elif args.load_history is not None:
4630                histData = trader.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
4631
4632                if args.render_chart is not None and histData is not None:
4633                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4634                    trader.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
4635
4636                    trader.ShowHistoryChart(
4637                        candles=histData,
4638                        interact=iChart,
4639                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4640                    )
4641
4642            elif args.trade is not None:
4643                if 1 <= len(args.trade) <= 5:
4644                    trader.Trade(
4645                        operation=args.trade[0],
4646                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
4647                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
4648                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
4649                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
4650                    )
4651
4652                else:
4653                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4654
4655            elif args.buy is not None:
4656                if 0 <= len(args.buy) <= 4:
4657                    trader.Buy(
4658                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
4659                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
4660                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
4661                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
4662                    )
4663
4664                else:
4665                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4666
4667            elif args.sell is not None:
4668                if 0 <= len(args.sell) <= 4:
4669                    trader.Sell(
4670                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
4671                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
4672                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
4673                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
4674                    )
4675
4676                else:
4677                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4678
4679            elif args.order:
4680                if 4 <= len(args.order) <= 7:
4681                    trader.Order(
4682                        operation=args.order[0],
4683                        orderType=args.order[1],
4684                        lots=int(args.order[2]),
4685                        targetPrice=float(args.order[3]),
4686                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
4687                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
4688                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
4689                    )
4690
4691                else:
4692                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
4693
4694            elif args.buy_limit:
4695                trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
4696
4697            elif args.sell_limit:
4698                trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
4699
4700            elif args.buy_stop:
4701                if 2 <= len(args.buy_stop) <= 7:
4702                    trader.BuyStop(
4703                        lots=int(args.buy_stop[0]),
4704                        targetPrice=float(args.buy_stop[1]),
4705                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
4706                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
4707                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
4708                    )
4709
4710                else:
4711                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4712
4713            elif args.sell_stop:
4714                if 2 <= len(args.sell_stop) <= 7:
4715                    trader.SellStop(
4716                        lots=int(args.sell_stop[0]),
4717                        targetPrice=float(args.sell_stop[1]),
4718                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
4719                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
4720                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
4721                    )
4722
4723                else:
4724                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
4725
4726            # elif args.buy_order_grid is not None:
4727            #     # update order grid work with api v2
4728            #     if len(args.buy_order_grid) == 2:
4729            #         orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
4730            #
4731            #         for order in orderParams:
4732            #             trader.Order(operation="Buy", lots=order["lot"], price=order["price"])
4733            #
4734            #     else:
4735            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4736            #
4737            # elif args.sell_order_grid is not None:
4738            #     # update order grid work with api v2
4739            #     if len(args.sell_order_grid) >= 2:
4740            #         orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
4741            #
4742            #         for order in orderParams:
4743            #             trader.Order(operation="Sell", lots=order["lot"], price=order["price"])
4744            #
4745            #     else:
4746            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4747
4748            elif args.close_order is not None:
4749                trader.CloseOrders(args.close_order)  # close only one order
4750
4751            elif args.close_orders is not None:
4752                trader.CloseOrders(args.close_orders)  # close list of orders
4753
4754            elif args.close_trade:
4755                if not (args.ticker or args.figi):
4756                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4757                    raise Exception("Ticker or FIGI required")
4758
4759                if args.ticker:
4760                    trader.CloseTrades([args.ticker])  # close only one trade by ticker (priority)
4761
4762                else:
4763                    trader.CloseTrades([args.figi])  # close only one trade by FIGI
4764
4765            elif args.close_trades is not None:
4766                trader.CloseTrades(args.close_trades)  # close trades for list of tickers
4767
4768            elif args.close_all is not None:
4769                trader.CloseAll(*args.close_all)
4770
4771            elif args.limits:
4772                if args.output is not None:
4773                    trader.withdrawalLimitsFile = args.output
4774
4775                trader.OverviewLimits(show=True)
4776
4777            elif args.user_info:
4778                if args.output is not None:
4779                    trader.userInfoFile = args.output
4780
4781                trader.OverviewUserInfo(show=True)
4782
4783            elif args.account:
4784                if args.output is not None:
4785                    trader.userAccountsFile = args.output
4786
4787                trader.OverviewAccounts(show=True)
4788
4789            else:
4790                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
4791                raise Exception("There is no command to execute")
4792
4793    except Exception:
4794        trace = tb.format_exc()
4795        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
4796            if e in trace:
4797                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
4798                break
4799
4800        uLogger.debug(trace)
4801        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
4802        exitCode = 255  # an error occurred, must be open a ticket for this issue
4803
4804    finally:
4805        finish = datetime.now(tzutc())
4806
4807        if exitCode == 0:
4808            if args.more:
4809                uLogger.debug("All operations were finished success (summary code is 0).")
4810
4811        else:
4812            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
4813                os.path.abspath(uLog.defaultLogFile), exitCode,
4814            ))
4815
4816        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
4817        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
4818            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4819            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4820        ))
4821        uLogger.debug("=-" * 50)
4822
4823        if not kwargs:
4824            sys.exit(exitCode)
4825
4826        else:
4827            return exitCode

Main function for work with TKSBrokerAPI in the console.

See examples: